Page 2 of 14

Como instalar o servidor FTP (vsftpd) no Ubuntu 14.04

Se você quiser configurar um servidor ftp seguro, eficiente e rápido, então você pode considerar instalar o vsftpd. É, provavelmente, o servidor FTP mais seguro e rápido para sistemas UNIX-like.

Por se pequeno para fins de velocidade e segurança, muitas configurações de FTP mais complicados são realizáveis com vsftpd. Por exemplo, é possível lidar com:

  • Configurações de IP virtuais
  • Os usuários virtuais
  • Operação independente ou inetd
  • Configuração poderosa por usuário
  • largura de banda
  • Per-source-IP configuração
  • Per-source-IP limites
  • IPv6
  • O suporte à criptografia SSL através da integração

Utilizando o passo a passo a seguir como guia para a sua configuração, você evita de se deparar com error como:

  • Error: The data connection could not be established: EHOSTUNREACH – No route to host
  • Error: Connection timed out
  • Error: Failed to retrieve directory listing

Instalando vsFTPd

  
sudo apt-get update  
sudo apt-get install vsftpd  

Configurando o vsFTPd

O servidor vai ser executado com privilégios mínimos de o usuário “nobody” e registro completo será ativado. Finalmente, vamos limitar clientes para os tempos de espera de ligação específicas e limitar os comandos que aceitará de todos os clientes FTP.

  
sudo vim /etc/vsftpd.conf  
  
#######################################################
###  vsftpd.conf   BEGIN
#######################################################
# manpage: http://vsftpd.beasts.org/vsftpd_conf.html
#
# Run in daemon mode
background=YES  
listen=YES  
#listen_address=111.111.111.111
#pasv_address=127.0.0.1
#
# The new highly restrictive seccomp filter sandbox
# If you see "OOPS: priv_sock_get_cmd" then disable seccomp
seccomp_sandbox=NO  
#
# User to run daemon as
#nopriv_user=_vsftpd
#ftp_username=_ftp
#
# Ftp ports
connect\_from\_port\_20=NO  
ftp\_data\_port=20  
listen_port=21  
pasv_enable=Yes  
pasv\_min\_port=7000  
pasv\_max\_port=7014  
pasv\_addr\_resolve=NO  
pasv_address=191.237.128.149  
pasv_promiscuous=NO  
port_enable=NO  
port_promiscuous=NO  
#
# SSL (force options for a SSL only server)
#ssl_enable=YES
#ssl_tlsv1=YES
#ssl_sslv2=NO
#ssl_sslv3=NO
#rsa_cert_file=/etc/ssl/private/vsftpd.pem
#allow_anon_ssl=YES
#force_anon_data_ssl=YES
#force_anon_logins_ssl=YES
#force_local_data_ssl=YES
#force_local_logins_ssl=YES
#
# Timeouts
connect_timeout=0  
data\_connection\_timeout=0  
idle\_session\_timeout=0  
#
# Information messages
#setproctitle_enable=YES
#banner_file=/etc/banner
dirmessage_enable=YES  
ftpd_banner=Teste ftp server  
#
# Access limits and controls
async\_abor\_enable=NO  
#cmds_allowed=ABOR,APPE,CWD,DELE,HELP,LIST,MDTM,MKD,NLST,PASS,PASV,PWD,QUIT,RETR,RMD,RNFR,RNTO,SIZE,STOR,TYPE,USER
#cmds_allowed=ABOR,CWD,DELE,LIST,MDTM,MKD,NLST,PASS,PASV,PWD,QUIT,RETR,RMD,RNFR,RNTO,SIZE,STOR,TYPE,USER,ACCT,APPE,CDUP,HELP,MODE,NOOP,REIN,STAT,STOU,STRU,SYST
delay\_successful\_login=1  
delete\_failed\_uploads=yes  
guest_enable=NO  
write_enable=YES  
max_clients=100  
max\_login\_fails=1  
max\_per\_ip=0  
pam\_service\_name=vsftpd  
allow\_writeable\_chroot=YES  
tcp_wrappers=NO  
hide_file={.*,*.mp3}  
deny_file={*.mp3}  
#
# Preferences
ascii\_upload\_enable=NO  
ascii\_download\_enable=NO  
hide_ids=YES  
ls\_recurse\_enable=NO  
use_localtime=NO  
#
# Allow anonymous FTP?
#anonymous_enable=YES
#anon_max_rate=0
#anon_mkdir_write_enable=NO
#anon_root=/disk01/ftp/
#anon_world_readable_only=YES
#anon_umask=0022
#anon_upload_enable=NO
#anon_other_write_enable=NO
#no_anon_password=NO
#
# Allow local user access?
local_enable=YES  
local\_max\_rate=0  
local_umask=0022  
chroot\_local\_user=YES  
check_shell=NO  
chmod_enable=NO  
secure\_chroot\_dir=/var/run/vsftpd/empty  
#userlist_enable=YES
userlist_deny=NO  
#userlist_file=/etc/vsftpd_users
#
# Logging
dual\_log\_enable=NO  
log\_ftp\_protocol=NO  
vsftpd\_log\_file=/var/log/vsftpd.log  
xferlog_enable=YES  
xferlog\_std\_format=NO  
xferlog_file=/var/log/xferlog  
#
# Passive

#
#######################################################
###  vsftpd.conf  END
#######################################################

Ativando o modo Passivo

Para isso nós reservar um intervalo de portas para usar em conexões FTP passivas. No exemplo abaixo, nós iremos utilizar o intervalo da porta 7000 até 7014. Normalmente é comum ser utilizado um range de portas bem maior, porém no quando você utiliza azure, existe uma limitação de 150 endpoints, por isso, vamos utilizar um range menor. Para ativar o modo passivo, será preciso editar o arquivo de configuração vsftpd.conf novamente:

  
sudo vi /etc/vsftpd.conf  

Adicionando as linhas abaixo:

  
pasv_enable=YES  
pasv\_min\_port=7000  
pasv\_max\_port=7014  
port_enable=YES  

Configurando o Firewall

Você vai precisar para ativar dois módulos do kernel para o iptables. Estes irão ligar o NAT (Network Address Translation) para FTP e rastreamento de conexão FTP. Como iptables / Netfilter é parte do kernel, precisamos usar modprobe para adicioná-los para a sessão atual, afinal você não vai querer reiniciar um servidor só por isso, e também fazer alterações em /etc/modules para que, caso seja necessário reiniciar o seu servidor mais tarde, os módulos sejam carregados durante a inicialização do mesmo.

Primeiro, nós deveremos utilizar o modprobe para usar esses dois módulos agora:

  
sudo modprobe ip_nat_ftp  
sudo modprobe ip_conntrack_ftp  

Em seguida, vamos modificar o arquivo de configuração /etc/modules assim os módulos deverão ser carregados na próxima reinicialização:

  
sudo vim /etc/modules  

Adicione as linhas:

  
ip\_nat\_ftp  
ip\_conntrack\_ftp  
  
sudo iptables -A INPUT -p tcp --destination-port 7000:7014 -j ACCEPT  

Adicionando usuários com acesso a pastas específicas

Pode ser necessário criar usuários com acesso a pastas específicas do seu servidor de FTP, para isso crie o usuário e atribua o home do mesmo para a pasta desejada.
Como meu servidor web está rodando nessa pasta estou adicionando o usuário para o grupo www-data

  
useradd -m usuarioEspecifico -s /usr/sbin/nologin -d /home/ftp/www/pasta/especifica/ -g www-data  

Garantindo permissão de escrita

Para garantir que o grupo possa escrever na pasta

  
sudo chmod go+rwx /home/ftp/www/pasta/especifica/  

Liberando as portas

Amazon EC2

Isto pode ser feito através do AWS Management Console (painel de controle web EC2 da Amazon), ou no seu próprio console:

ec2-authorize default -p 20-21
ec2-authorize default -p 7000-7014

Azure

É necessário adicionar as seguintes portas abertas no
EndPoint:
TCP 20-21
TCP 7000-7014

Reinicie o serviço vsftpd

Finalmente, você deve ter tudo que você precisa para se conectar via FTP no seu servidor:

  
sudo service vsftpd restart  

Como definir o fuso horário no Ubuntu

Uma das primeiras coisas que você deve fazer quando instala um servidor linux, é definir o fuso horário, afinal, é muito chato quando você tem que ir para o arquivo de log e encontrar todas as timestamps estão em UTC. Mas podemos definir o fuso horário do servidor para que quando seu SGBD cron e etc fiquem no mesmo horário em que o seu computador, tornando-os mais realistas.

Você pode verificar o fuso horário atual, apenas executando o comando:

  
$ date

Thu Dec 18 17:06:17 MST 2014

Para trocar a o fuso horário, basta executar o comando:

  
$ sudo dpkg-reconfigure tzdata

E selecionar o continente e o fuso horário que preferir.

Também não se esqueça de reiniciar cron, afinal, como ele não vai detectar a mudança do fuso horário ele continuará executando os seus agendamentos em UTC.

Nginx error 413: Request entity too large

Através desse post rápido hoje abordaremos um erro bastante comum com Nginx: o infame 413 Request entity too large. Eu tive o problema agora pouco quando estava fazendo um mini-sistema de upload de arquivos na minha máquina de desenvolvimento, então eu pensei que eu iria fazer um post no blog sobre isso para me certificar de que não se esqueça disso na próxima vez.

Error 413: O que isso significa?

Esse erro aparece quando um visitante envia muitos dados na solicitação HTTP, no meu caso o tamanho do arquivo que eu estava fazendo upload era grande demais.
Isso normalmente é causada por:
• um arquivo muito grande sendo carregado pelo visitante.
• dados POST grandes demais sendo enviado pelo cliente.
A correção é tão simples como a criação de uma directiva na sua configuração Nginx, leia abaixo.

Como corrigí-lo? client_max_body_size

Para corrigir isso, você precisa aumentar o valor da diretiva client_max_body_size. Esta diretiva define a quantidade máxima de dados que o Nginx aceita em uma solicitação HTTP. Por padrão, esse valor é definido como 1 megabyte, ou seja, se você tentar carregar um arquivo maior do que 1 megabyte você estará recebendo um erro 413: Request entity too large. Você pode inserir esta diretiva em três níveis:

No bloco http: isto irá definir o valor diretiva para todos os servidores e os locais na sua configuration.
No bloco de server: isto irá definir o valor directiva para todos os locais de um determinado servidor .
No bloco location: isto irá definir o valor directiva para um local específico em um determinado servidor .

Neste exemplo, etaremos utilizando a diretiva no bloco http e vamos configurá-lo para 250 megabytes:

http {  
    client_max_body_size 250M; # permite o upload de arquivos de até 500 megabytes
    [...]
}

É necessário fazer mais alguma coisa?

Depois de alterar a configuração do servidor, não se esqueça de recarregar Nginx para que a configuração seja aplicada. Há também algumas definições de configuração que você deverá mudar, caso você esteja usando PHP. Caso não esteja utilizando PHP, apenas ignore o resto deste post. Você vai precisar para abrir seu arquivo de configuração php.ini e procurar dois valores:

  • upload_max_filesize: Tamanho máximo permitido para arquivos carregados (padrão: 2 megabytes). Você precisa aumentar esse valor se você espera por arquivos com mais de 2 megabytes de tamanho.
  • post_max_size: tamanho máximo dos dados POST que o PHP irá aceitar (padrão: 8 megabytes). Os arquivos são enviados através de dados POST, então você precisa aumentar esse valor se você está esperando por arquivos com mais de 8 megabytes.

Após fazer as alterações, não se esqueça de recarregar PHP-FPM, para que as alterações sejam aplicadas corretamente. Isso é tudo! Você não deve estar mais recebendo erros de upload depois disso.

Como Instalar a Plataforma de Blog Ghost no Ubuntu

Depois de mudar meu blog da kinghost para a DigitalOcean, eu resolvi mudar também a plataforma que eu utilizava para as postagens. Então, resolvi mudar do wordpress para o Ghost.

Primeiro, vamos verificar a configuração do mysql.

Verificando MySql

Antes de criarmos os bancos, é importante verficarmos se nossa instalação do MySql está utilizando utf8. No arquivo /etc/mysql/my.cnf verifique se a ele possui as linhas character_set_server e collation_server.

[mysqld]
[....]
character_set_server = utf8  
collation_server = utf8_general_ci  
[....]

Feito isso, podemos prosseguir para a instalação propriamente dita, e para tal vamos criar os bancos de dados e um usuário para o ghost.

Criando usuário e bancos de dados

$ mysql -u root -p

No prompt de comando mysql> vamos utilizar os seguintes comandos. Certifique-se de trocar “SUA_SENHA” com sua própria
senha, afinal, essa vai ser a senha do seu banco de dados que utilizaremos para o Ghost.
Vamos criar dois bancos de dados, um ghost e um ghostdev, um para produção e outro para desenvolvimento, respectivamente.
Criaremos um usuário ghost que terá acesso aos bancos, lembre-se de substituir @’localhost’ por @’%’, caso você deseje ter acesso remoto à esses bancos.
Atribuiremos privilégios para que os usuários possam acessar os bancos.

create database ghostdev;  
create database ghost;  
create user 'ghost'@'localhost' identified by 'SUA_SENHA';  
grant all privileges on ghost.* to 'ghost'@'localhost';  
grant all privileges on ghostdev.* to 'ghost'@'localhost';  
flush privileges;  
quit  

Instalando o Nodejs

$ sudo apt-get install npm nodejs

Instalando o Ghost

Primeiro, vamos entrar na pasta, criar a pasta blog

$ mkdir /var/www/blog/
$ cd /var/www/blog/

Agora, vamos baixar o ghost utilizando o curl.

$ curl -L https://ghost.org/zip/ghost-latest.zip -o ghost.zip

Vamos descompactar o arquivo.

$ unzip -uo ghost.zip -d .
$ cd ghost

Instalaremos o Node.js no Ghost. Você também vai querer o módulo para MySQL, para que a nossa instalação do Ghost utilize o banco de dados que criamos anteriormente.

$ npm install --production
$ npm install mysql

Modifique os config.js em seu Ghost. Altere a url nas sessões de desenvolvimento e produção para o endereço do seu blog.

development: {  
    // The url to use when providing links to the site, E.g. in RSS and email.
    url: 'http://blog.adlerdias.eti.br',

e

production: {  
    url: 'http://blog.adlerdias.eti.br',

Em seguida, colocaremos as configurações do banco de dados de produção e desenvolvimento, substituindo as versões SQLite padrão, tanto em desenvolvimento e produção:

database: {  
    client: 'mysql',
    connection: {
        host: 'localhost',
        user: 'ghost',
        password: 'SUA_SENHA',
        database: 'ghostdev',
        charset: 'utf8'
    }
},

e, para Produção:

database: {  
    client: 'mysql',
    connection: {
        host: 'localhost',
        user: 'ghost',
        password: 'SUA_SENHA',
        database: 'ghost',
        charset: 'utf8'
    }
},

Para finalizar, vamos instalar e iniciar o Ghost.

$ npm install --production
$ npm start

Em um navegador, navegue até a url 127.0.0.1:2368 para ver o seu Blog.

Gzip Requisições HTTP com NGINX

Se você utiliza já utiliza o Nginx, provavelmente você se importa com desempenho, e uma das maneiras mais fáceis de ganhar desempenho é compactar em Gzip as requisições HTTP. O Gzip pode não só aumentar o desempenho do seu site, como também economizar a banda.

Como o Gzip funciona

De uma forma bem simplificada, a compressão em gzip funciona, encontrando strings semelhantes dentro de um arquivo de texto, e substituindo aquelas strings temporariamente para fazer o tamanho total do arquivo menor. Esta forma de compressão é particularmente adequada para a web porque o HTML e CSS geralmente contém muitas strings repetidas, como espaços em branco, tags e definições de css.

Se você utiliza o Nginx no Linux, você, provavelmente já possui o gzip instalado.

Você pode adicionar essas diretivas no http, server ou ainda na location do seu arquivo de configuração do nginx, dependendo apenas das suas necessidades.

gzip on;  
gzip_proxied any;  
gzip_types text/plain text/css text/javascript text/xml application/xml application/xhtml+xml application/xml+rss;  
gzip_disable "MSIE [1-6].";  

Explicando as diretivas

gzip on; – Habilitamos a compressão Gzip.
gzipproxied any; – Aplica a compressão Gzip em todas as requisções. Você pode ver a lista com todos os parâmetros aqui – gzipproxied.
gziptypes text/plain text/css text/javascript text/xml application/xml application/xhtml+xml application/xml+rss; – Tipos de conteúdo que serão comprimidos com Gzip. Perceba que text/html já é habilitado por padrão, e irá gerar um aviso se você adicioná-lo aqui.
gzip*disable “MSIE [1-6].” – Serve para desabilitarmos a compressão com Gzip para as versões muito antigas do Internet Explorer.

Outras Configurações Gzip

Existem algumas outras configurações que podem ser utilizadas que não foram listadas acima. Ou seja, você pode personalizá-lo ainda mais. Eu não vejo qualquer razão para mudar estes dos valores padrão, mas acredito que valha a pena citá-los.

gzipcomplevel

Você pode aumentar o nível de compressão, pagando o preço do trabalho do servidor. O nível padrão é 1, que é o que a maioria das pessoas utilizam. É geralmente aceito que o aumento gzipcomplevel é um anti-pattern de desempenho, uma vez que leva mais tempo para comprimir do que a economia que será gerada por essa compressão extra.

gzipminlength

Com essa diretiva, você pode definir o tamanho mínimo do arquivo em bytes para que o mesmo seja comprimido utilizando Gzip. O valor padrão é de 20 bytes.

Se você quiser ler mais sobre Gzipping com o servidor nginx, recomendo a documentação oficial.

Redirecionamento 301 NGINX

Nginx é um grande servidor web, é também o servidor utilizado por este blog. Eu já utilizava o Nginx no meu ambiente de desenvolvimento e agora que eu resolvi mudar de vez da kinghost para a DigitalOcean, foi a chance que eu precisava para utilizá-lo também por aqui, para tal, eu tive que criar uma série de redirecionamentos tanto para melhorar a experiência do usuário evitando que os usuários fossem direcionados para páginas 404 quanto para indexação do motor de busca. Este post vai mostrar como criar alguns redirecionamentos 301 permanentes em seu arquivo de configuração Nginx.

Sempre que possível evite redirecionar páginas, mantendo as mesmas url’s quando você refaz um site. Mas se ainda assim você precisar redirecionar domínios, diretŕoios ou páginas em um servidor Nginx esse post vai te ajudar.

NGINX – Redirecionando Domínios

Para os sites novos, ter o www antes de seu domínio não é realmente necessário. Aqui está como redirecionar a versão www do seu site para a versão limpa, ou seja, a versão sem www.

Redirecionamento WWW para sem WWW


server   {
   server_name www.seudominio.eti.br;
   rewrite  ^/(.*)$  http://seudominio.eti.br/$1 permanent;
}

Redirecionamento sem WWW para WWW

Da mesma maneira, se o seu site já está publicado há bastante tempo, e utilizando a versão www, você pode ter certeza que os usuários serão redirecionados para a versão www.


server   {
   server_name seudominio.eti.br;
   rewrite  ^/(.*)$  http://www.seudominio.eti.br/$1 permanent;
}

NGINX Redirecionamento de Diretório

Pode ser necessário redirecionar todo um diretório de páginas para outro se você renomeá-lo. Caso você tenha que alterar a estrutura do site, possivelmente, você terá um conjunto desses em seu arquivo de configuração.

Essa diretiva vai dentro do bloco do servidor principal de seus sites de configuração NGINX.


if ( $request_filename ~ diretorio-antigo/.+ ) {
       rewrite ^(.*) http://seudominio.com/diretorio-novo/$1 permanent;
   }

if ( $request_filename ~ diretorio-antigo/.+ ) {
       rewrite ^(.*) http://seudominio.com/diretorio-novo/$1 permanent;
   }

Caso seja necessário, você pode simplesmente remover o diretório, dessa maneira:


if ( $request_filename ~ diretorio-antigo/.+ ) {
       rewrite ^(.*) http://seudominio.com/$1 permanent;
   }

Você pode reescrever a mesma regra acima, utilizando apenas uma linha, dessa forma:


location / {
    rewrite ^/diretorio-antigo/(.*) /$1 permanent;
}

NGINX Redirecionamento de Página

Quando você reconstruir um site, é comum para remover as páginas, ou renomeá-los. Se a página não tem links de outros sites, você não quer que os usuários que clicarem no link para terra em uma página de erro 404. A melhor coisa a fazer neste caso é enviar o usuário para a página seguinte mais útil.

Utilize essa diretriz no bloco de servidor para redireccionar páginas únicas.


if ( $request_filename ~ pagina-antiga/ ) {
      rewrite ^ http://seudominio.com/proxima-pagina/? permanent;
}

Você também pode utilizar o redirecionamento de página, para disponibilizar o arquivo robots.txt e o sitemap.xml.


location ~ ^/(sitemap.xml) {
    root /var/www/blog.adlerdias.eti.br/public;
}

location ~ ^/(robots.txt) {
    root /var/www/blog.adlerdias.eti.br/public;
}

abraço,
até a próxima

File(/usr/bin/php) is not within the allowed path(s)

open_basedir – O que é?

No PHP existe uma diretiva chamada openbasedir, que é uma medida de proteção que previne que usuários abram arquivos ou scripts que estão localizados fora do diretório home com PHP, a não ser que a pasta esteja especificada como excluída. A diretiva openbasedir se ativada, irá garantir que todas as operações em arquivos sejam limitadas à arquivos que estão dentro de uma certa hierarquia de pastas, e assim, evitar que scripts consigam acessar arquivos em uma pasta que não esteja autorizada. Quando um script tenta abrir um arquivo com, por exemplo, fopen() or gzopen(), a localização do arquivo é verificada. Quando o arquivo está fora da árvore de diretórios permitida, o php irá recusar abrir esse arquivo e algum dos erros abaixos poderá acontecer:

[adlersd@insomniac:~/www]
% composer  
PHP Warning:  Phar::mapPhar(): open_basedir restriction in effect. File(/usr/local/bin/composer) is not within the allowed path(s): (/srv/http/:/home/:/tmp/:/usr/share/pear/:/usr/share/webapps/:/home/adlersd/www/) in /usr/local/bin/composer on line 13

Warning: Phar::mapPhar(): open_basedir restriction in effect. File(/usr/local/bin/composer) is not within the allowed path(s): (/srv/http/:/home/:/tmp/:/usr/share/pear/:/usr/share/webapps/:/home/adlersd/www/) in /usr/local/bin/composer on line 13  

ou

[ErrorException]
  is_file(): open_basedir restriction in effect. File(/usr/bin/php) is not within the allowed p  
  ath(s): (/srv/http/:/home/:/tmp/:/usr/share/pear/:/usr/share/webapps/)

ou ainda

[adlersd@insomniac:~/www/sf2]
% composer                                                                255 ↵
PHP Warning:  Phar::mapPhar(): open_basedir restriction in effect. File(/usr/local/bin/composer) is not within the allowed path(s): (/srv/http/:/home/:/tmp/:/usr/share/pear/:/usr/share/webapps/) in /usr/local/bin/composer on line 13

Warning: Phar::mapPhar(): open_basedir restriction in effect. File(/usr/local/bin/composer) is not within the allowed path(s): (/srv/http/:/home/:/tmp/:/usr/share/pear/:/usr/share/webapps/) in /usr/local/bin/composer on line 13  
PHP Warning:  require(phar://composer.phar/bin/composer): failed to open stream: phar error: invalid url or non-existent phar "phar://composer.phar/bin/composer" in /usr/local/bin/composer on line 15

Warning: require(phar://composer.phar/bin/composer): failed to open stream: phar error: invalid url or non-existent phar "phar://composer.phar/bin/composer" in /usr/local/bin/composer on line 15  
PHP Fatal error:  require(): Failed opening required 'phar://composer.phar/bin/composer' (include_path='.:/usr/share/pear') in /usr/local/bin/composer on line 15

Fatal error: require(): Failed opening required 'phar://composer.phar/bin/composer' (include_path='.:/usr/share/pear') in /usr/local/bin/composer on line 15  

Como eu utilizo open_basedir para resolver o meu problema?

Precisamos editar o php.ini, no meu caso, /etc/php/php.ini

[adlersd@insomniac:~/]
% sudo vim /etc/php/php.ini

e modificar a linha abaixo adicionando os paths que estavam nos alertas/erros acima:

open_basedir = /srv/http/:/home/:/tmp/:/usr/share/pear/:/usr/share/webapps/:/usr/bin/:/usr/local/bin/  

Como criar um sistema de autenticação (login) de usuário – parte 1

Nesse post, você vai aprender como criar o seu próprio sistema de autenticação de usuário com o banco de dados, essa é uma dúvida muito comum quando se está começando a lidar com o framework.

Existem uma série de extensões, módulos e sistemas RBAC disponíveis nas extensões Yii e, claro, o sistema RBAC do próprio framework. Porém, muitas vezes eles são destinados a projetos mais complexos, ou ainda, são muito complicados de integrá-los a sua solução quando você ainda está começando a caminhar e entender como o framework funciona.

Esse post é uma sugestão do Luciano.

Há muito tempo atrás eu criei um “projeto” para explicar alguns conceitos no yii. Abaixo seguem os links para você que chegou aqui, agora, não ficar perdido.

  1. Yii – Entendendo os relacionamentos (relations)
  2. Yii – Como criar um dropdown
  3. Yii – Como criar um filtro de um relacionamento
  4. Yii – Como fazer o botão delete do CGridView/TbGridView funcionar usando Ajax

Vamos utilizar essa mesma estrutura, e dar continuidade a esse “projeto”.

O que nós vamos construir

Quando você cria o seu projeto ou aplicação, no yii, automaticamente já está criado um sistema de autenticação de usuário muito simples que pode ser facilmente estendido para fornecer uma autenticação de usuário através do banco de dados. A partir de agora nós vamos construir um sistema de autenticação ou login, com nível de usuário, onde esse mesmo usuário pode ser usuário ou administrador e nós vamos ser capazes de usar esses níveis para limitar o acesso a funções dentro de nossas aplicações. As senhas de usuário serão tratadas de forma diferente, por enquanto basta você saber que não salvaremos as mesmas em texto puro, mas eu falarei sobre ela no seu devido tempo. Os usuários serão obrigados a alterar suas senhas depois de um intervalo de tempo definido.

Entendendo as diferenças entre WebUser e Autenticação

Esta primeira parte será a de entender a diferença entre a autenticação do usuário e do WebUser(identidade do usuário). Informações do usuário são armazenados em uma instância da classe CWebUser e este é criado na inicialização do aplicativo (ou seja: quando o usuário conecta pela primeira vez com o site), independentemente de o usuário estar autenticado (logado) ou não. Por padrão, o usuário é definido como Guest (visitante).
A autenticação é gerenciada por uma classe chamada CUserIdentity e essa classe verifica se o usuário é conhecido e se é um usuário válido. Como esta validação ocorre vai depender de sua aplicação, talvez aconteça no seu banco de dados – que será o nosso caso – ou entrar com facebook/google/github, ou contra um servidor ldap, etc…
O código gerado pelo Gii define a ação de login e modelo LoginForm que gerencia esse processo para nós e une essas duas classes juntas. No login, o sistema cria uma classe UserIdentity, passando os detalhes de login. Estes são validados, no nosso caso, como já citado, em nosso banco de dados.

Então o modelo de login passa o objeto UserIdentity ao objeto CWebUser, que, em seguida, armazena essa informação. Por padrão, a classe CWebUser armazena suas informações em dados da sessão e, portanto, não deve conter informações confidenciais, como senhas, por exemplo.

Nosso model de Usuário

CREATE TABLE `tbl_usuarios` (  
  `id` int(11) NOT NULL AUTO_INCREMENT,
  `ds_username` varchar(128) NOT NULL,
  `ds_password` varchar(128) NOT NULL,
  `ds_email` varchar(128) NOT NULL,
  `ds_nome` varchar(128) DEFAULT NULL,
  `nu_paginacao` tinyint(3) NOT NULL DEFAULT '25',
  `cd_empresa` int(9) DEFAULT NULL,
  `cd_filial` int(9) DEFAULT NULL,
  `role` int(1) NOT NULL DEFAULT '0',
  `dt_criacao` datetime DEFAULT NULL,
  `dt_alteracao` datetime DEFAULT NULL,
  `dt_ultima_visita` datetime DEFAULT NULL,
  `status` int(1) NOT NULL DEFAULT '0',
  `dt_expiracao_senha` datetime DEFAULT NULL,
  PRIMARY KEY (`id`),
  UNIQUE KEY `ds_username` (`ds_username`),
  UNIQUE KEY `ds_password` (`ds_password`),
  KEY `status` (`status`),
  KEY `superuser` (`role`),
  KEY `fk_usuario_empresa` (`cd_empresa`),
  KEY `fk_usuario_filial` (`cd_filial`),
  CONSTRAINT `fk_usuario_filial` FOREIGN KEY (`cd_filial`) REFERENCES `tbl_filial` (`cd_filial`),
  CONSTRAINT `fk_usuario_empresa` FOREIGN KEY (`cd_empresa`) REFERENCES `tbl_emp` (`cd_emp`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;

Agora você deve utilizar o Gii para gerar o CRUD.

Personalizando o model do Usuário

Agora precisamos atualizar o modelo do usuário da seguinte forma:
1. Incluir constantes para representar nossos papéis.
2. Criptografar a senha
3. Use beforeSave para verificar senhas e atualizar a senha data de validade
4. Adicione quaisquer relações relevantes para a sua aplicação

/**
 * This is the model class for table "tbl_usuarios".
 *
 * The followings are the available columns in table 'tbl_usuarios':
 * @property integer $id
 * @property string $ds_username
 * @property string $ds_password
 * @property string $ds_email
 * @property string $ds_nome
 * @property integer $nu_paginacao
 * @property integer $cd_empresa
 * @property integer $cd_filial
 * @property integer $role
 * @property string $dt_criacao
 * @property string $dt_alteracao
 * @property string $dt_ultima_visita
 * @property integer $status
 * @property string $dt_expiracao_senha
 *
 * The followings are the available model relations:
 * @property TblFilial $cdFilial
 * @property TblEmp $cdEmpresa
 */
class Usuario extends CActiveRecord  
{
    const ROLE_USUARIO      = 3;
    const ROLE_ADMIN        = 1;
    const PASSWORD_EXPIRY   = 90;
    public $passwordSave;
    public $repeatPassword;
    /**
     * @return string the associated database table name
     */
    public function tableName()
    {
        return 'tbl_usuarios';
    }

    /**
     * @return array validation rules for model attributes.
     */
    public function rules()
    {
        // NOTE: you should only define rules for those attributes that
        // will receive user inputs.
        return array(
            //array('ds_password', 'compare', 'compareAttribute'=>'repeatPassword', 'message'=>'Por favor digite as senhas iguais.'),
            array('ds_username, ds_password, ds_email', 'required'),
            array('nu_paginacao, cd_empresa, cd_filial, role, status', 'numerical', 'integerOnly'=>true),
            array('ds_username, ds_password, ds_email, ds_nome', 'length', 'max'=>128),
            array('dt_criacao, dt_alteracao, dt_expiracao_senha', 'safe'),
            // The following rule is used by search().
            // @todo Please remove those attributes that should not be searched.
            array('id, ds_username, ds_password, ds_email, ds_nome, nu_paginacao, cd_empresa, cd_filial, role, dt_criacao, dt_alteracao, dt_ultima_visita, status, dt_expiracao_senha', 'safe', 'on'=>'search'),
        );
    }

    public function beforeSave() {
        parent::beforeSave();
        // Criptografamos a senha se for um novo registro
        if ($this->isNewRecord) {
            $this->ds_password = md5($this->ds_password);
            $this->dt_criacao=new CDbExpression("NOW()");
            $this->dt_expiracao_senha=new CDbExpression("DATE_ADD(NOW(), INTERVAL ".self::PASSWORD_EXPIRY." DAY) ");
        }
        else if (!empty($this->ds_password)&&!empty($this->repeatPassword)&&($this->ds_password===$this->repeatPassword))
        // Se não for um registro novo, salva a senha apenas se as duas senhas casarem.
        {
            $this->ds_password = md5($this->ds_password);
            $this->dt_expiracao_senha=new CDbExpression("DATE_ADD(NOW(), INTERVAL ".self::PASSWORD_EXPIRY." DAY) ");
        }
        return true;
    }

    /**
    * Compare Expiry date and today's date
    * @return type - positive number equals valid user
    */
    public function checkExpiryDate() {
        $expDate=DateTime::createFromFormat('Y-m-d H:i:s',$this->password_expiry_date);
        $today=new DateTime("now");
        fb($today->diff($expDate)->format('%a'),"PASSWORD EXPIRY");
        return ($today->diff($expDate)->format('%a'));
    }

    /**
     * @return array relational rules.
     */
    public function relations()
    {
        // NOTE: you may need to adjust the relation name and the related
        // class name for the relations automatically generated below.
        return array(
            'relationFilial' => array(self::BELONGS_TO, 'Filial', 'cd_filial'),
            'relationEmpresa' => array(self::BELONGS_TO, 'Empresa', 'cd_empresa'),
        );
    }

    /**
     * @return array customized attribute labels (name=>label)
     */
    public function attributeLabels()
    {
        return array(
            'id' => 'id',
            'ds_username' => 'Usuário',
            'ds_password' => 'Senha',
            'repeatPassword' => 'Repita a Senha', 
            'ds_email' => 'Email',
            'ds_nome' => 'Nome',
            'nu_paginacao' => 'Paginacao',
            'cd_empresa' => 'Empresa',
            'cd_filial' => 'Filial',
            'role' => 'Role',
            'dt_criacao' => 'Dt Criacao',
            'dt_alteracao' => 'Dt Alteracao',
            'dt_ultima_visita' => 'Dt Ultima Visita',
            'status' => 'Status',
            'dt_expiracao_senha' => 'Dt Expiracao Senha',
        );
    }

    /**
     * Retrieves a list of models based on the current search/filter conditions.
     *
     * Typical usecase:
     * - Initialize the model fields with values from filter form.
     * - Execute this method to get CActiveDataProvider instance which will filter
     * models according to data in model fields.
     * - Pass data provider to CGridView, CListView or any similar widget.
     *
     * @return CActiveDataProvider the data provider that can return the models
     * based on the search/filter conditions.
     */
    public function search()
    {
        // @todo Please modify the following code to remove attributes that should not be searched.

        $criteria=new CDbCriteria;

        $criteria->compare('id',$this->id);
        $criteria->compare('ds_username',$this->ds_username,true);
        $criteria->compare('ds_password',$this->ds_password,true);
        $criteria->compare('ds_email',$this->ds_email,true);
        $criteria->compare('ds_nome',$this->ds_nome,true);
        $criteria->compare('nu_paginacao',$this->nu_paginacao);
        $criteria->compare('cd_empresa',$this->cd_empresa);
        $criteria->compare('cd_filial',$this->cd_filial);
        $criteria->compare('role',$this->role);
        $criteria->compare('dt_criacao',$this->dt_criacao,true);
        $criteria->compare('dt_alteracao',$this->dt_alteracao,true);
        $criteria->compare('dt_ultima_visita',$this->dt_ultima_visita,true);
        $criteria->compare('status',$this->status);
        $criteria->compare('dt_expiracao_senha',$this->dt_expiracao_senha,true);

        return new CActiveDataProvider($this, array(
            'criteria'=>$criteria,
        ));
    }

    /**
     * Returns the static model of the specified AR class.
     * Please note that you should have this exact method in all your CActiveRecord descendants!
     * @param string $className active record class name.
     * @return Usuarios the static model class
     */
    public static function model($className=__CLASS__)
    {
        return parent::model($className);
    }
}

A classe UserIdentity

Em seguida, vamos alterar a classe UserIdentity original gerado Gii para autenticar o usuário contra a nossa nova tabela do usuário. Isto é encontrado em protected/components/UserIdentity.php e será parecido com este:

/**
 * UserIdentity represents the data needed to identity a user.
 * It contains the authentication method that checks if the provided
 * data can identity the user.
 */
class UserIdentity extends CUserIdentity  
{
    private $_id;
    private $_username;

    public function getName()
    {
        return $this->_username;
    }

    public function getId()
    {
        return $this->_id;
    }

    public function authenticate()
    {
        $user = Usuario::model()->find('LOWER(ds_username)=?', array(strtolower($this->username)));
        if($user === null)
        {
            $this->errorCode= self::ERROR_UNKNOWN_IDENTITY;
        }
        elseif($user->ds_password !== md5($this->password))
        {
            $this->errorCode= self::ERROR_PASSWORD_INVALID;
        }
        else
        {
            $this->_id = $user->id;
            $this->_username = $user->ds_email;
            $user->dt_ultima_visita=new CDbExpression("NOW()");
            $user->save();
            $this->errorCode= self::ERROR_NONE;
        }
        return !$this->errorCode;
    }
}

Personalizando a Classe WebUser

Uma vez que o usuário foi validado, no nosso caso, no banco de dados, podemos carregar os detalhes do usuário para a classe webuser que Yii detém a sessão do usuário. Posto isso, você deve tomar cuidado com as informações que você carrega e armazena na classe webuser, afinal, a sessão pode ser facilmente acessada pelo usuário final. Então, vamos criar um novo componente chamado WebUser em protected/components/

class WebUser extends CWebUser {  
    // Store model to not repeat query.
    private $_model;
    // Return first name.
    // access it by Yii::app()->user->first_name
    function getNome(){
        $user = $this->loadUser(Yii::app()->user->id);
        return $user->ds_nome;
    }
    function getRole(){
        $user = $this->loadUser(Yii::app()->user->id);
        return $user->role;
    }
    function getPaginacao(){
        $user = $this->loadUser(Yii::app()->user->id);
        return $user->nu_paginacao;
    }

    function getDtExpiracao(){
        $user = $this->loadUser(Yii::app()->user->id);
        return $user->checkExpiryDate();
    }

    // verificamos o valor do atributo role com a constante
    // ROLE_ADMIN para sabermos se o usuário é administrador
    function isAdmin(){
        $user = $this->loadUser(Yii::app()->user->id);
        if ($user!==null)
            return intval($user->role) == Users::ROLE_ADMIN;
        else return false;
    }

    // verificamos o valor do atributo role com a constante
    // ROLE_USUARIO para sabermos se o usuário é administrador
    function isUser(){
        $user = $this->loadUser(Yii::app()->user->id);
        if ($user!==null)
            return intval($user->role) == Users::ROLE_USUARIO;
        else return false;
    }

    // carrega o model do usuário
    protected function loadUser($id=null) {
        if($this->_model===null)
        {
            if($id!==null)
                $this->_model=Users::model()->findByPk($id);
        }
        return $this->_model;
    }
}

E então, no principal arquivo de configuração protected/config/main.php) você terá que especificar a classe do componente do usuário, por exemplo:

    [ ... ]
    'components'=>array(
        'user'=>array(
            // enable cookie-based authentication
            'allowAutoLogin'=>true,
            'loginUrl' => array('/site/login'),
            'class'=>'WebUser',
        ),
    [ ... ]

Nossa classe WebUser estende a classe CWebUser que é a classe acessado pelo componente Usuário da nossa aplicação. Assim, quando você referência Yii::app()->user agora irá referenciar a nossa nova classe WebUser. Portanto, tudo o que nós definimos aqui podem ser acessados usando a sintaxe
Yii::app()->user->propriedade ou Yii::app()->user->funcao()
Exemplo: Yii::app()->user->isAdmin()

Definimos um campo de perfil de usuário chamado nu_paginacao, que será utilizado para permitir que os usuários definam quantos itens são exibidos em um GridView. Por motivos de desempenho, então pode ser uma boa idéia definir um limite máximo para não acabar com o desempenho da aplicação.

Personalizando nosso CRUD

Vamos arrumar o formulário protected/views/usuario/_form.php da seguinte forma:

<div class="form">

<?php $form=$this->beginWidget('CActiveForm', array(  
    'id'=>'usuario-form',
    // Please note: When you enable ajax validation, make sure the corresponding
    // controller action is handling ajax validation correctly.
    // There is a call to performAjaxValidation() commented in generated controller code.
    // See class documentation of CActiveForm for details on this.
    'enableAjaxValidation'=>false,
)); ?>

    <p class="note">Fields with <span class="required">*</span> are required.</p>

    <?php echo $form->errorSummary($model); ?>

    <div class="row-fluid">
        <?php echo $form->labelEx($model,'ds_username'); ?>
        <?php echo $form->textField($model,'ds_username',array('size'=>60,'maxlength'=>128)); ?>
        <?php echo $form->error($model,'ds_username'); ?>
    </div>

    <div class="row-fluid">
        <div class="span6">
            <?php echo $form->labelEx($model,'ds_password'); ?>
            <?php echo $form->passwordField($model,'ds_password',array('size'=>60,'maxlength'=>128)); ?>
            <?php echo $form->error($model,'ds_password'); ?>
        </div>
        <div class="span6">
            <?php echo $form->labelEx($model,'repeatPassword'); ?>
            <?php echo $form->passwordField($model,'repeatPassword',array('size'=>60,'maxlength'=>128)); ?>
            <?php echo $form->error($model,'repeatPassword'); ?>
        </div>
    </div>

    <div class="row-fluid">
        <div class="span6">
            <?php echo $form->labelEx($model,'ds_email'); ?>
            <?php echo $form->textField($model,'ds_email',array('size'=>60,'maxlength'=>128)); ?>
            <?php echo $form->error($model,'ds_email'); ?>
        </div>
        <div class="span6">
            <?php echo $form->labelEx($model,'ds_nome'); ?>
            <?php echo $form->textField($model,'ds_nome',array('size'=>60,'maxlength'=>128)); ?>
            <?php echo $form->error($model,'ds_nome'); ?>
        </div>
    </div>

    <div class="row-fluid">
        <?php echo $form->labelEx($model,'nu_paginacao'); ?>
        <?php echo $form->textField($model,'nu_paginacao'); ?>
        <?php echo $form->error($model,'nu_paginacao'); ?>
    </div>

    <div class="row-fluid">
        <div class="span6">
            <?php echo $form->labelEx($model,'cd_empresa'); ?>
            <?php echo $form->textField($model,'cd_empresa'); ?>
            <?php echo $form->error($model,'cd_empresa'); ?>
        </div>
        <div class="span6">
            <?php echo $form->labelEx($model,'cd_filial'); ?>
            <?php echo $form->textField($model,'cd_filial'); ?>
            <?php echo $form->error($model,'cd_filial'); ?>
        </div>
    </div>

    <div class="row-fluid">
        <?php echo $form->labelEx($model,'role'); ?>
        <?php echo $form->textField($model,'role'); ?>
        <?php echo $form->error($model,'role'); ?>
    </div>

    <div class="row buttons">
        <?php echo CHtml::submitButton($model->isNewRecord ? 'Criar' : 'Salvar'); ?>
    </div>

<?php $this->endWidget(); ?>

</div><!-- form -->  

Para logar utilize admin/admin.

Por enquanto é só, vamos continuar com mais detalhes amanhã! Para baixar o projeto como está, clique aqui.

Como utilizar namespaces em php – parte 1

Namespaces já existem em outras liguagens de programação há muito tempo, essa é uma das mudanças mais significativas no PHP 5.3. Desenvolvedores Java e C#, além de outras linguagens, já estão familiarizados com isso, e agora com a utilização de Namapaces a estrutura de aplicações PHP mudará para melhor. Então, o que é um namespace no PHP? Em suma, é um recipiente abstrato que nos permite reutilizar a mesma função, classe e nomes de constantes, mas se aplicam diferentes significados com base em que contexto eles estão inseridos.

Não dá pra tornar a explicação mais fácil?

Imagine um namespace como uma gaveta na qual você pode colocar todos os tipos de coisas: um caderno, uma caneta, um lápis, uma borracha… Estes são os seus pertences. Diretamente debaixo de sua gaveta é de outra pessoa, e ele coloca as mesmas coisas nele. Para evitar o uso de itens de cada um, você decide colocar uma etiqueta nas gavetas para deixar claro o que pertence a quem.
Anteriormente, os desenvolvedores tiveram que usar underscore em suas classes, funções e constantes para separar bases de código. Isso é equivalente à nossa etiqueta.
Essencialmente, um namespace não é nada mais do que um bloco de código hierarquicamente rotulado que contém código PHP normal.

Por que precisamos de Namespaces?

Conforme a sua biblioteca de código vai crescendo, há um aumento do risco de acidentalmente, você acabar redefinindo uma função ou nome de classe que já foi utilizada anteriormente. O problema é agravado quando você tenta adicionar componentes ou plugins de terceiros, por exemplo: o que aconteceria se dois ou mais componentes utilizassem a classe Usuário?
Até agora, a única solução foi alongar os nomes de classe/função de comprimento. Por exemplo, o wordpress utiliza o prefixo ‘wp_'(underscore).
Problemas de colisão de nomes pode ser resolvido com namespaces. Constantes, classes e funções podem ser agrupadas em namespaces diferentes.

Como namespaces são definidos?

Por padrão, todoas as constante, classes e nomes de função são colocados no espaço global – como eram antes do PHP suportar namespace.
O namespace é definido apenas pela palavra-chave namespaceno topo do seu arquivo PHP. Ele deve ser o primeiro comando (com a exceção do declare) e nenhum código não-PHP, HTML, ou de espaço em branco pode preceder essa palavra-chave.

Vamos dar uma olhada em um pequeno trecho de código, e então vamos quebrar e expandir o que está acontecendo em pequenas partes, para que fique mais fácil de entender.

namespace Foo;

function Bar()  
{
    echo __NAMESPACE__;
}

namespace FooFoo;

function Bar()  
{
    echo "Bar";
}

echo \Foo\Bar();  

Saída:

Foo

Existem alguns pontos interessantes aqui:
A palavra-chave namespace
A constante NAMESPACE
A redefinição da função Bar()
A estranha nova sintaxe no comando echo no final do arquivo: a resolução de nomes

A palavra-chave namespace

Namespaces são nomeados arbitrariamente e têm duas formas de sintaxe. Em tempo, em qualquer uma das sintaxes, namespaces não podem ser aninhados, embora, sub-namespaces possam ser definidos, como será explicado mais adiante.

Sintaxe 1

namespace Foo;

// bloco de código

Sintaxe 2

namespace Foo  
{
    // bloco de código
}

A utilização de Namespaces torna mais fácil evitar definições conflitantes e introduzir mais flexibilidade e organização dos seus códigos.
Perceba também que que as chaves em torno do bloco de código são completamente opcionais. Na verdade, aderindo à regra de utilizar um namespace por arquivo e omitindo as chaves você torna seu código muito mais limpo.

A constante __NAMESPACE__

A constante NAMESPACE contém uma seqüência que exibe o namespace atual. Quando chamado a partir do contexto global, uma string vazia é exibida.

<?php  
namespace Blog;  
echo __NAMESPACE__;  
?>

Saída:

Blog

A utilização de namespaces tem benefícios bastante óbvios durante a depuração. Também pode ser usada para gerar dinamicamente um nome de classe completamente qualificado. Por exemplo:

<?php  
namespace Blog;

class MinhaClasse {  
    public function WhoAmI() {
        return __METHOD__;
    }
}

$c = __NAMESPACE__ . '\MinhaClasse';
$m = new $c;
echo $m->WhoAmI();  
?>

Saída:

Blog\MinhaClasse::WhoAmI

AngularJs – Como começar se já sei jQuery

1. Não projete a sua página, para depois alterá-la com manipulações DOM

Em jQuery, você cria uma página, e então você começa a torná-la dinâmica. Isso ocorre porque o jQuery foi projetado para o acréscimo e tem crescido incrivelmente partir dessa premissa simples.

Por outro lado, em AngularJS, você deve começar a partir do zero, com sua arquitetura em mente. Em vez de começar por pensar “eu tenho esse pedaço do DOM e eu quero fazê-lo fazer X”, você tem que começar com o que você quer realizar, em seguida, ir sobre o projeto de sua aplicação, e, finalmente, ir sobre o projeto de seu ponto de vista.

2. Não aumente jQuery com AngularJS

Da mesma forma, não comece com a idéia de que o jQuery faz X, Y e Z, por isso vou adicionar AngularJS em cima disso para os models e controllers. Isso é muito tentador quando você está apenas começando, e é por isso que sempre é recomendo que os novos desenvolvedores AngularJS não utilizem jQuery em tudo, pelo menos até que se acostumarem a fazer as coisas da “Jeito Angular”.

É muito mais comum do que se pensa ver desenvolvedores criando soluções elaboradas com plugins jQuery de 150 ou 200 linhas de código que, depois, colá-las em AngularJS com uma coleção de callbacks e $applys que são confusas e complicadas, mas, eles eventualmente vão conseguir fazer isso funcionar! O problema é que na maioria dos casos esse plugin jQuery poderia ser reescrito em AngularJS em uma fração do código, onde de repente tudo se torna compreensível e simples.

A questão de fundo é esta: quando você está projetando uma solução, primeiro “pense em AngularJS”, se você não pode pensar em uma solução, faça uma pesquisa no google, vá em busca de ajuda na comunidade, se depois de tudo isso, você não conseguir encontrar uma solução, então sinta-se livre para utilizar o jQuery. Mas não deixe que jQuery tornar-se uma muleta ou você nunca vai dominar AngularJS.

3. Sempre pense em termos de arquitetura

Em primeiro lugar saber que as aplicações de uma única página são aplicações. Eles não são páginas da web. Então, precisamos pensar como um desenvolvedor do lado do servidor(server-side), além de pensar como um desenvolvedor do lado do cliente(client-side). Temos que pensar em como dividir a nossa aplicação, em, componentes testáveis, extensíveis e individuais.

Então, como você faz isso? Como você “pensa em AngularJS”? Aqui estão alguns princípios gerais, em contraste com jQuery.

3.1 O ponto de vista é o “official record

Em jQuery, nós programaticamente alteramos nossa view. Poderíamos ter um menu dropdown definido como um ul assim:

<ul class="main-menu">  
    <li class="active">
        <a href="#">Home</a>
    </li>
    <li>
        <a href="#">Menu 1</a>
        <ul>
            <li><a href="#">Submenu 1</a></li>
            <li><a href="#">Submenu 2</a></li>
            <li><a href="#">Submenu 3</a></li>
        </ul>
    </li>
    <li>
        <a href="#">Menu 2</a>
    </li>
</ul>  

Em jQuery, em nossa lógica de aplicação, gostaríamos de ativá-lo com algo como:

$('.main-menu').dropdownMenu();

Quando nós basta olhar para o ponto de vista, não é imediatamente óbvio que há alguma funcionalidade aqui. Para pequenas aplicações, isso é bom. Mas para aplicações não-triviais, as coisas rapidamente se confuso e difícil de manter.

Em AngularJS, porém, a vista é o official record de funcionalidade view-based. Nossa declaração ul ficaria assim:

<ul class="main-menu" dropdown-menu>  
    ...
</ul>  

Estes dois fazem a mesma coisa, mas na versão AngularJS alguém olhando para o model sabe o que é deverá acontecer. Sempre que um novo membro da equipe de desenvolvimento vem a bordo, ela pode olhar para isso e, em seguida, saber que existe uma diretiva chamado DropDownMenu operando nela, ela não precisa intuir a resposta certa ou vasculhar todo o código. A visão nos disse o que era suposto acontecer. Muito mais limpo.

Desenvolvedores novos em AngularJS muitas vezes perguntam algo como: como faço para encontrar todos os links de um tipo específico e adicionar uma diretiva sobre eles. O desenvolvedor é sempre espantado quando têm sua pergunta respondida: Você não… Mas a razão pela qual você não deve fazer isso é que isto é como meia jQuery, meio-AngularJS, e não é bom. O problema aqui é que o desenvolvedor está tentando “fazer jQuery” no contexto da AngularJS. Isso nunca vai funcionar bem. A vista é o official record. Fora de uma diretiva (mais sobre isso abaixo), você nunca, nunca, nunca muda o DOM. E as diretrizes são aplicadas na visão, por isso a intenção é clara.

Lembre-se: não se projetar, e, em seguida, marcar. Você deve arquiteto e design.

3.2 Data binding

Esta é, de longe, uma das características mais impressionantes do AngularJS e diminui muito a necessidade de fazer o tipo de manipulações DOM que foram mencionadas na seção anterior. AngularJS irá atualizar automaticamente a sua visão, logo, você não precisa fazer isso! Em jQuery, nós devemos esperar a resposta de um evento e, em seguida atualizar o conteúdo. Algo como:

$.ajax({
  url: '/myEndpoint.json',
  success: function ( data, status ) {
    $('ul#log').append('<li>Data Received!</li>');
  }
});

Para a view teríamos algo parecido com:

<ul class="messages" id="log">  
</ul>  

Além das interesses de mistura, também temos os mesmos problemas de significar a intenção que foram mencionadas anteriormente. Mas o mais importante, tivemos a referência e atualizar um nó DOM manualmente. E se quisermos eliminar uma entrada de log, temos de código contra o DOM para isso também. Como é que podemos testar a lógica além do DOM? E se nós queremos mudar a apresentação?

Este pouco confuso e um pouco frágil. Mas, em AngularJS, podemos fazer isso dessa maneira:

$http( '/myEndpoint.json' ).then( function ( response ) {
    $scope.log.push( { msg: 'Data Received!' } );
});

Para a view teríamos algo parecido com:

<ul class="messages">  
    <li ng-repeat="entry in log">{{ entry.msg }}</li>
</ul>  

Mas, também poderíamos ter a view dessa maneira:

<div class="messages">  
    <div class="alert" ng-repeat="entry in log">
        {{ entry.msg }}
    </div>
</div>  

E agora, em vez de utilizarmos uma lista não ordenada, estamos usando caixas de alerta do Twitter Bootstrap. E nunca se fez necessária qualquer alteração no código do controlador! Mas o mais importante, não importa onde ou como o registro é atualizado, a visão vai mudar também. Automaticamente.

Embora não tenha exibido o mesmo aqui, a ligação de dados é bidirecional. Então, essas mensagens de log também podem ser editadas na exibição apenas fazendo isso:

<input ng-model="entry.msg" />  

3.3 Camada modelo distinto

Em jQuery, o DOM é uma espécie de model. Mas, em AngularJS, temos uma camada de modelo separada que nós podemos controlar de qualquer forma que for necessária, de forma completamente independente do ponto de vista. Isso ajuda para a ligação de dados acima, mantém a separação de interesses, e apresenta muito maior testabilidade.

3.4 Separação de interesses

Você deve manter as seus interesses em separado. Sua visão funciona como o official record do que é deve acontecer (pelo menos, na maioria das vezes), o model representa os dados, você tem uma camada de serviço para executar tarefas reutilizáveis; você faz manipulação DOM e acrescenta a sua view com as diretivas, e você junta tudo isso com os controladores.

A única coisa que se faz necessário acrescentar pertence à testabilidade, que será discutida em outro trecho mais abaixo.

3.5 Injeção de Dependência

Para nos ajudar com separação de interesses é a injeção de dependência(DI). Se você vem de uma linguagem server-side (de Java à PHP), você provavelmente está familiarizado com este conceito já, mas se você é um cara do lado do cliente vindo de jQuery, este conceito pode parecer bobo. Mas não é.

A partir de uma perspectiva ampla, DI significa que você pode declarar componentes muito livremente e, em seguida, a partir de qualquer outro componente, basta pedir uma instância dele e ele vai será concedido. Você não tem que saber sobre a ordem de carregamento, ou locais de arquivo, ou qualquer coisa assim. O poder não pode ser imediatamente visível, um exemplo comum seria teste.

Vamos dizer que em sua aplicação, precisamos de um serviço que implementa o armazenamento do lado do servidor por meio de uma API REST e, dependendo do estado do aplicativo, armazenamento local também. Ao executar testes em nossos controladores, não quero ter que comunicar com o servidor – estamos testando o controlador, depois de tudo. Nós podemos apenas adicionar um serviço de simulação do mesmo nome do nosso componente original, e, o injector irá garantir que o nosso controlador recebe um falso automaticamente – o nosso controlador não precisa e não saberá a diferença.

4 Desenvolvimento dirigido a testes (TDD) – Sempre

É impossível negar que isso é realmente parte da seção 3, na arquitetura, mas é tão importante que é melhor ser colocado em sua própria seção.

Fora de todos os muitos plugins jQuery que você já viu, utilizou, ou escreveu, quantos deles tinham um conjunto de testes que o acompanha? Não muitos, porque jQuery não é muito favorável a isso. Mas AngularJS é.

Em jQuery, a única maneira de testar é muitas vezes para criar o componente de forma independente com uma página de amostra / demo contra o qual nossos testes pode executar a manipulação de DOM. Então nós temos que desenvolver um componente separadamente e depois integrá-lo em nossa aplicação. Assim, grande parte do tempo, ao desenvolver com jQuery, optamos por uma maneira de desenvolvimento iterativa, em vez de dirigida a testes(tdd).

Mas porque temos separação de interesses, podemos fazer o desenvolvimento orientado a testes de forma iterativa em AngularJS! Por exemplo, vamos dizer que queremos uma diretiva super-simples para indicar nosso menu que nossa rota atual. Podemos declarar o que queremos em nossa view:

<a href="#" when-active>Hello</a>  

Agora você pode escrever o seu teste:

it( 'should add "active" when the route changes', inject(function() {  
    var elm = $compile( '<a href="/hello" when-active>Hello</a>' )( $scope );

    $location.path('/not-matching');
    expect( elm.hasClass('active') ).toBeFalsey();

    $location.path( '/hello' );
    expect( elm.hasClass('active') ).toBeTruthy();
}));

Rodando o teste podemos confirmar que ele falha. Então agora podemos escrever a nossa diretiva:

.directive( 'whenActive', function ( $location ) {
    return {
        scope: true,
        link: function ( scope, element, attrs ) {
            scope.$on( '$routeChangeSuccess', function () {
                if ( $location.path() == element.attr( 'href' ) ) {
                    element.addClass( 'active' );
                }
                else {
                    element.removeClass( 'active' );
                }
            });
        }
    };
});

Agora o teste passa e nosso menu executa conforme solicitado. Nosso desenvolvimento é iterativo e dirigido a testes.

5. Conceitualmente, as diretivas não são “pacotes” jQuery

Você vai ouvir muitas vezes “só fazem manipulação DOM em uma diretiva”. Isso é uma necessidade. Então, trate com o devido respeito!

Algumas diretivas apenas decoram o que já está na view (pense em ngClass) e, portanto, às vezes, fazer manipulação DOM imediatamente e, em seguida, são basicamente feito. Mas, se a diretiva é como um “widget” e tem um model, ele também deve respeitar a separação de interesses. Ou seja, o model também deve permanecer em grande parte independente da sua implementação nos links e controllers.

AngularJS vem com um conjunto completo de ferramentas para fazer isto muito facilmente, com ngClass podemos atualizar dinamicamente a classe; ngBind permite a ligação de dados bidirecional; ngShow e ngHide programaticamente mostrar ou ocultar um elemento, e muitos mais – inclusive os que escrevemos nós mesmos. Em outras palavras, nós podemos fazer todos os tipos de grandiosidade sem manipulação DOM. Quanto menos manipulação DOM, será mais fácil testar as suas diretivas, será mais fácil estilizá-las, será mais fácil de modificá-las no futuro, e mais re-utilizável e distribuível eles são.

É fácil perceber que muitos desenvolvedores novos em AngularJS utilizando diretivas como o lugar para jogar um monte de jQuery. Em outras palavras, eles pensam “já que eu não posso fazer manipulação DOM no controlador, eu vou levar esse código colocá-lo numa diretiva”. Enquanto que, certamente, é muito melhor, muitas vezes é ainda errado.

Pense no logger da seção 3. Mesmo que você coloque isso em uma diretiva, você ainda pode fazê-lo do “jeito Angular”. Ele ainda não requer qualquer manipulação DOM! Há muitas ocasiões em que a manipulação DOM é necessária, mas é muito mais raro do que você pensa! Antes de fazer a manipulação DOM em qualquer lugar em seu aplicativo, pergunte-se se você realmente precisa. Pode haver uma maneira melhor.

Aqui está um exemplo que mostra o padrão que possivelmente será encontrado com mais freqüência. Queremos um botão alternável. Em tempo, este exemplo é um pouco artificial para representar os casos mais complicados, que são resolvidos da mesma maneira.

.directive( 'myDirective', function () {
    return {
        template: '<a class="btn">Toggle me!</a>',
        link: function ( scope, element, attrs ) {
            var on = false;

            $(element).click( function () {
                if ( on ) {
                    $(element).removeClass( 'active' );
                }
                else {
                    $(element).addClass( 'active' );
                }

                on = !on;
            });
        }
    };
});

Existem algumas coisas de errado com isso. Primeiro, jQuery nunca foi necessário. Não há nada que fiz aqui que precisava jQuery! Segundo, mesmo que já utilizássemos jQuery na nossa página, não há nenhuma razão para usá-lo aqui, podemos simplesmente utilizar o angular.element e nosso componente continuará funcionando mesmo em um projeto que não tem jQuery. Em terceiro lugar, mesmo assumindo jQuery foi necessário para a presente directiva para o trabalho, jqLite(angular.element) sempre utilizará jQuery se ele foi carregado! Então, não precisamos usar o $ – podemos apenas usar angular.element. O quarto lugar, está intimamente relacionado com a terceiro, é que os elementos jqLite não necessitam estar envolvidos com $ – o elemento que é passado para a função de ligação já constituiria um elemento jQuery! E quinto, que já foi mencionado em seções anteriores, porque estamos misturando coisas do modelo em nossa lógica?

Esta diretiva poderia ser reescrita de maneira mais simples, assim:

.directive( 'myDirective', function () {
    return {
        scope: true,
        template: '<a class="btn" ng-class="{active: on}" ng-click="toggle()">Toggle me!</a>',
        link: function ( scope, element, attrs ) {
            scope.on = false;

            scope.toggle = function () {
                scope.on = !$scope.on;
            };
        }
    };
});

Mais uma vez, tudo que for modelo deve ficar no modelo, de modo que você (ou seus usuários) pode facilmente trocá-lo por um que atenda a qualquer estilo necessário, e a lógica nunca teve de ser tocado. Reutilização.

E ainda há todos os outros benefícios, como o teste – é fácil! Não importa o que está no modelo, API interna da diretiva nunca é tocada, dessa maneira a refatoração é fácil. Você pode alterar o modelo, tanto quanto você quiser, sem tocar na directiva. E não importa o que você mude, seus testes ainda passar.

Então, se diretivas não são apenas coleções de funções jQuery, quais são eles? Diretivas são extensões de HTML. Se o HTML não fizer algo que você precisa fazer, você escreve uma diretiva para fazer isso por você e, em seguida, passa a utilizá-la como se fosse parte de HTML.

Colocando isso de outra maneira, se AngularJS não fazer algo, pense em como a equipe iria realizá-lo para combinar perfeitamente com ngClick, ngClass…

© 2017 Adler Dias

Theme by Anders NorénUp ↑