Tagyii framework

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.

Yii – Como fazer o botão delete do CGridView/TbGridView funcionar usando Ajax

No post de hoje você vai entender como você pode modificar a função de delete do CGridView para funcionar com ajax, exibindo a mensagem de erro, ou, de sucesso de uma maneira mais amigável. Para isso nós primeiro vamos alterar o nosso método delete dos nossos controllers Empresa e Filial. Essa parte não têm segredo, nós utilizamos try/catch para garantirmos a manipulação de erros, e, verificamos se a chamada foi ajax, ou não.

Uma parte muito importante do desenvolvimento de software é a usabilidade, e, se preocupar como o usuário receberá alertas é também muito importante.

Vamos continuar o desenvolvimento do nosso “projetinho” com cadastro de Empresa e Filial.

  1. Yii – Entendendo os relacionamentos (relations)
  2. Yii – Como criar um dropdown
  3. Yii – Como criar um filtro de um relacionamento

Alterando o actionDelete do controller da Empresa

controller/EmpresaController.php

  
public function actionDelete($id)  
{
    try
    {
        $this->loadModel($id)->delete();
        if(!isset($_GET['ajax']))
            Yii::app()->user->setFlash('success','Empresa removida com sucesso.');
        else
            echo '
×Sucesso! Empresa removida com sucesso.
'; } catch(CDbException $e) { if(!isset($_GET['ajax'])) Yii::app()->user->setFlash('error','Não foi possível remover essa Empresa.'); else echo '
×Erro! Não foi possível remover essa Empresa.
'; } // if AJAX request (triggered by deletion via admin grid view), we should not redirect the browser if(!isset($_GET['ajax'])) $this->redirect(isset($_POST['returnUrl']) ? $_POST['returnUrl'] : array('admin')); }

controller/FilialController.php

  
public function actionDelete($id)  
{
    try
    {
        $this->loadModel($id)->delete();
        if(!isset($_GET['ajax']))
            Yii::app()->user->setFlash('success','Filial removida com sucesso.');
        else
            echo '
×Sucesso! Filial removida com sucesso.
'; } catch(CDbException $e) { if(!isset($_GET['ajax'])) Yii::app()->user->setFlash('error','Não foi possível remover essa Filial.'); else echo '
×Erro! Não foi possível remover essa Filial.
'; } // if AJAX request (triggered by deletion via admin grid view), we should not redirect the browser if(!isset($_GET['ajax'])) $this->redirect(isset($_POST['returnUrl']) ? $_POST['returnUrl'] : array('admin')); }

Agora que você já tratou as requisições via POST e via AJAX, nós devemos alterar a nossa view admin.php, dessa forma, quando o retorno vier, ele será exibido corretamente. Aproveitando já vamos exibir os tooltips e a confirmação da opção de remover traduzidos. Quando tratamos o retorno exibimos a mensagem de erro ou sucesso, esperamos 5 segundos, então fechamos o alerta.

views/empresa/admin.php

  
breadcrumbs=array(
    'Empresa'=>array('index'),
    'Gerenciar',
);
$this->menu=array(
    array('label'=>'Listar Empresa','url'=>array('index')),
    array('label'=>'Criar Empresa','url'=>array('create')),
);
?>

Gerenciar Empresa

user->hasFlash('success')):?>
× Sucesso! user->getFlash('success'); ?>
user->hasFlash('error')):?>
× Erro! user->getFlash('error'); ?>
widget('bootstrap.widgets.TbGridView',array( 'id'=>'empresa-grid', 'dataProvider'=>$model->search(), 'filter'=>$model, 'columns'=>array( 'cd_emp', 'ds_nome', 'nu_cnpj', 'dt_inativacao', array( 'header' => 'Ações', 'class'=>'bootstrap.widgets.TbButtonColumn', 'template'=>'{view}{update}{delete}', 'buttons' => array( 'view' => array( 'label'=>'Exibir', ), 'update' => array( 'label'=>'Editar', ), 'delete' => array( 'label'=>'Remover', ), ), 'afterDelete'=> 'function(link,success,data) { if(success) { $("#statusMsg").html(data); setTimeout(function() { $(".alert").delay(1000).alert("close") }, 5000); } }', 'deleteConfirmation'=>"js:'Tem certeza que deseja remover a empresa: '+$(this).parent().parent().children(':nth-child(2)').text()+'?'", ), ), )); ?>

views/filial/admin.php

  
breadcrumbs=array(
    'Filial'=>array('index'),
    'Gerenciar',
);

$this->menu=array(
    array('label'=>'Listar Filial','url'=>array('index')),
    array('label'=>'Criar Filial','url'=>array('create')),
);
?>

Gerenciar Filial

user->hasFlash('success')):?>
× Sucesso! user->getFlash('success'); ?>
user->hasFlash('error')):?>
× Erro! user->getFlash('error'); ?>
widget('bootstrap.widgets.TbGridView',array( 'id'=>'filial-grid', 'dataProvider'=>$model->search(), 'filter'=>$model, 'columns'=>array( 'cd_filial', 'ds_nome', 'ds_cnpj', 'ds_uf', 'ds_cidade', array( 'name' => 'cd_empresa', 'value' => '$data->relationEmpresa->ds_nome', ), 'dt_inativacao', array( 'header' => 'Ações', 'class'=>'bootstrap.widgets.TbButtonColumn', 'template'=>'{view}{update}{delete}', 'buttons' => array( 'view' => array( 'label'=>'Exibir', ), 'update' => array( 'label'=>'Editar', ), 'delete' => array( 'label'=>'Remover', ), ), 'afterDelete'=> 'function(link,success,data) { if(success) { $("#statusMsg").html(data); setTimeout(function() { $(".alert").delay(1000).alert("close") }, 5000); } }', 'deleteConfirmation'=>"js:'Tem certeza que deseja remover a empresa: '+$(this).parent().parent().children(':nth-child(2)').text()+'?'", ), ), )); ?>

Para baixar o projeto como está, clique aqui.

No próximo post da série, vamos fazer um botão adicional e faremos ativar/desativar a filial.

Yii – Como criar um filtro de um relacionamento

Hoje explicarei como fazer o filtro do CGridView funcionar utilizando o relacionamento implementado no post Yii – Entendendo os relacionamentos (relations) e no segundo post Yii – Como criar um dropdown, você pode continuar utilizando o mesmo fonte, caso, ainda não tenha feito, você pode fazer clicando aqui.

Onde utilizaremos o Relacionamento do Yii

Quando você usa o gii para gerar as views, por padrão, todos os campos exibidos são ligados ao modelo que você está vendo. Entretanto, muitas vezes, você não quer ver o valor de uma chave estrangeira, mas sim o valor que está associado(relacionado) a esse atributo(coluna). Todas as nossas views exibem o id da empresa no lugar do nome da empresa, logo primeiro faremos o básico, que é alterar o index e a view para que seja exibido corretamente o nome da empresa.

filial/_view

    <b><?php echo CHtml::encode($data->getAttributeLabel('cd_empresa')); ?>:</b>
    <?php echo CHtml::encode($data->relationEmpresa->ds_nome); ?>

filial/view

'attributes'=>array(  
    [...]
    'relationEmpresa.ds_nome',

Em nosso exemplo, o nosso CGridView exibe o valor de nossa chave estrangeira – ou do nosso relacionamento – para empresa, porém, nós queremos que seja exibido o nome da empresa em questão. Isso é muito fácil de corrigir, alterando apenas algumas partes do código gerado pelo gii.

Como vocês puderam perceber, em nosso exemplo, quando nos acessamos filial/admin não aparece o nome da empresa, mas sim seu id.

A primeira parte desse post trata exatamente disso, vamos utilizar o nosso relacionamento para exibir o nome da empresa, invés de mostrar o id da mesma. Para que possamos fazer isso vamos alterar a nossa view filial/admin:

filial/admin

'columns'=>array(  
    [...]
    // aqui devemos trocar a linha 'cd_empresa' pelo array abaixo
    array(
        'name' => 'cd_empresa',
        'value' => '$data->relationEmpresa->ds_nome',
    ),

O problema, no entanto, mesmo que estejamos exibindo agora o nome da empresa o filtro continua apenas funcionando pelo id do mesmo. Primeiro, você precisa mudar a forma como a comparação é feita no método search().

Essa alteração do método search() visa alterar a comparação do idempresa para o nome da empresa. Como você pode perceber agora é utilizado usando o “ponto” de notação, como em “state.name”. Esta é a sintaxe para consultar uma tabela com o alias de “relationEmpresa” para o valor de sua coluna “dsnome”.

models/Filial.php

search

    [...]
    $criteria->with=array('relationEmpresa');

    //$criteria->compare('cd_empresa',$this->cd_empresa);
    $criteria->compare('relationEmpresa.ds_nome',$this->cd_empresa, true);
    $criteria->together=true;
    [...]

A segunda parte da alteração diz respeito ao nosso método rules(). Você tem que declarar que a coluna “ds_nome” da tabela Empresa pode ser pesquisada(safe).

models/Filial.php

rules

'attributes'=>array(  
    [...]
    array('cd_filial, ds_nome, ds_cnpj, ds_uf, ds_cidade, relationEmpresa.ds_nome, dt_inativacao,', 'safe', 'on'=>'search'),

Para a nossa busca funcionar, isso é tudo que precisa ser feito.

Para baixar o projeto como está, clique aqui.

Yii – Como criar um dropdown

Agora continuaremos a desenvolver alterando a criação da filial, adicionando um dropdown para listar as empresas para seleção.

Primeiro vamos alterar as nossas actions create e update para pegar as empresas cadastradas, que serão utilizadas para preencher o nosso dropdown. Para isso utilizaremos o CDbCriteria, assim criaremos uma regra onde vamos exibir apenas as empresas que ainda não estiverem inativas.

controllers/Filial.php

actionCreate

public function actionCreate()  
{
    $model=new Filial;

    // Uncomment the following line if AJAX validation is needed
    // $this->performAjaxValidation($model);

    if(isset($_POST['Filial']))
    {
        $model->attributes=$_POST['Filial'];
        if($model->save())
            $this->redirect(array('view','id'=>$model->cd_filial));
    }

    $criteria = new CDbCriteria();
    $criteria->addCondition("dt_inativacao = '0000-00-00'");
    $criteria->order = "ds_nome ASC";
    $empresa = CHtml::listData(Empresa::model()->findAll($criteria), 'cd_emp', 'ds_nome');

    $this->render('create',array(
        'model'=>$model,
        'empresa'=>$empresa,
    ));
}

actionUpdate

public function actionUpdate($id)  
{
    $model=$this->loadModel($id);

    // Uncomment the following line if AJAX validation is needed
    // $this->performAjaxValidation($model);

    if(isset($_POST['Filial']))
    {
        $model->attributes=$_POST['Filial'];
        if($model->save())
            $this->redirect(array('view','id'=>$model->cd_filial));
    }

    $criteria = new CDbCriteria();
    $criteria->addCondition("dt_inativacao = '0000-00-00'");
    $criteria->order = "ds_nome ASC";
    $empresa = CHtml::listData(Empresa::model()->findAll($criteria), 'cd_emp', 'ds_nome');

    $this->render('update',array(
        'model'=>$model,
        'empresa'=>$empresa,
    ));
}

Como vocês podem notar, estamos passando mais um parâmetro para o nosso render. Ele é contém as empresas cadastradas no banco de dados que não estão inativas e serão utilizadas para preencher o nosso dropdown.

A variável empresa, agora, também está disponível na view, e devemos fazê-la chegar até o nosso filial/_form.php. Para isso nós modificaremos os arquivos filial/update.php e filial/create.php. Afinal, nas actions create e update do controller, nós estamos passando como parâmetro a variável empresa – que conforma já explicamos contém a lista das empresas sem data de inativação – agora, como é utilizado um renderPartial para o _form, precisamos fazer com que o valor da empresa, seja também passado para essa outra view, para fazermos isso, modificamos apenas o renderPartial.

views/filial

create.php

[...]
<?php echo $this->renderPartial('_form', array('model'=>$model, 'empresa'=>$empresa)); ?>  

update.php

[...]
<?php echo $this->renderPartial('_form', array('model'=>$model, 'empresa'=>$empresa)); ?>  

A última parte dessa alteração, é trocar o textFieldRow, pelo dropDownList, que será feito no arquivo filial/_form.php

Populando nosso dropdown

views/filial
_form.php

<?php //echo $form->textFieldRow($model,'dt_inativacao',array('class'=>'span5')); ?>  
<?php echo $form->labelEx($model,'cd_empresa'); ?>  
<?php echo $form->dropDownList($model,'cd_empresa', $empresa, array('class'=>'span5')); ?>  

Por enquanto é só isso, no próximo post, utilizaremos os relacionamentos criados no post anterior para exibir corretamente o nome da empresa, e para podermos utilizá-la no filtro do CGridview.

Para baixar o projeto como está, clique aqui.

Yii – Indirect modification of overloaded property

Quando eu estava tentando modificar um atributo de uma relation, estava apresentando o erro: Indirect modification of overloaded property.

Esse problema ocorre quando o atributo não é uma propriedade da classe. Por isso, está chamando o método mágico __get(), mas, como se trata de um relacionamento o retorno desse método mágico dá-se por valor e não por atributo, logo, a atribuição direta de um valor para essa propriedade não tem efeito.

Exemplo: Indirect modification of overloaded property.

$count=0;
foreach ($pedido->relationItem as $item)  
{
    $pedido->relationItem[$count]->DATA_ENVIO = $data_envio;
    $pedido->relationItem[$count]->ENVIADO = 1);
    $count++;
}

Você pode resolver esse erro utilizando o método setAttribute:

$count=0;
foreach ($pedido->relationItem as $item)  
{
    $pedido->relationItem[$count]->setAttribute('DATA_ENVIO',$data_envio);
    $pedido->relationItem[$count]->setAttribute('ENVIADO',1);
    $count++;
}

Yii – Como tratar exceções utilizando o Alert do Bootstrap.

No psot de hoje será mostrado como tratar exceções utilizando o alert do Bootstrap, assim, nosso alerta será exibido de uma forma mais amigável para o usuário do sistema. Primeiramente devemos configurar o nosso main/config.php para fazer a rota dos erros para a nossa view de exibição do erro, para isso, nós utilizaremos a propriedade CErrorHandler::errorAction. Caso o seu main.php não possua o array abaixo, é só implementá-lo.

Configurando nosso errorAction

return array(  
    ......
    'components'=>array(
        'errorHandler'=>array(
            'errorAction'=>'site/error',
        ),
    ),
);

Agora que nós configuramos a propriedade errorAction, para o Controller Site, nós precisamos de um action error. Para isso, nós devemos criá-lo manualmente, caso não exista, ou alterá-lo. Afinal, essa action será responsável por exibir os erros disparados ao tratar exceções.

Alterando nosso actionError

public function actionError()  
{
    if($error=Yii::app()->errorHandler->error)
        $this->render('error', $error);
}

Alteraremos a action Contact apenas para exibir uma mensagem de erro. Para podermos testar o nosso código.

public function actionContact()  
{
    throw new CHttpException(400, "Desculpe-nos o transtorno, porém, página solicitada não existe.");
}

Agora vamos alterar a nossa view para utilizar o alert do bootstrap ao tratar exceções. Como só podemos utilizar o TbAlert utilizando setFlash, nós utilizaremos o mesmo html gerado pelo setFlash.

<?php  
/* @var $this SiteController */
/* @var $error array */
$this->pageTitle=Yii::app()->name . ' - Error';
$this->breadcrumbs=array(
    'Error',
);
?>
<div class="alert in alert-block fade alert-error">  
    <a data-dismiss="alert" class="close">×</a>
    <strong><?php echo $code; ?></strong> <?php echo CHtml::encode($message); ?>
</div>  

Agora é só clicar no link Contact, que você irá ver erro exibido como o TbAlert do Yii Twitter Bootstrap.

Você pode baixar os fontes desse exemplo clicando aqui!

Yii – Como pegar o IP do visitante ( ou usuário registrado )

A variável userHostAddress possuem informações do servidor e do visitante, dentre todas as informações contidas nessa variável, também está o ip.

Para obter o endereço IP do usuário dentro do Framework Yii usar o seguinte código:

Como pegar o IP do visitante

echo Yii::app()->request->userHostAddress;  

Você pode pegar o valor dessa variável de qualquer lugar dentro de sua aplicação Yii Framework.

Post feito à partir do comentário do Gustavo.

Yii – Entendendo os relacionamentos (relations)

Vamos considerar a seguinte estrutura abaixo como um exemplo, para que possamos demonstrar a utilização da realtions HASMANY e BELONGSTO.

Relacionamento

A relação BELONGSTO diz que um campo neste modelo aponta para a chave primária em outro modelo, neste caso, o modelo atual possui o domínio de ligação.
A relação HAS
ONE diz que algum outro modelo tem um campo de ligação apontando para a chave principal deste modelo, neste caso, o modelo relacionado possui o campo de ligação.

Abaixo segue o sql para criação do exemplo:

CREATE TABLE `tbl_emp` (  
  `cd_emp` int(9) NOT NULL AUTO_INCREMENT,
  `ds_nome` varchar(20) NOT NULL,
  `nu_cnpj` varchar(15) DEFAULT NULL,
  `dt_inativacao` date DEFAULT NULL,
  PRIMARY KEY (`cd_emp`)
) ENGINE=InnoDB AUTO_INCREMENT=0 DEFAULT CHARSET=utf8;

CREATE TABLE `tbl_filial` (  
  `cd_filial` int(9) NOT NULL AUTO_INCREMENT,
  `ds_nome` varchar(20) NOT NULL,
  `ds_cnpj` varchar(15) DEFAULT NULL,
  `ds_uf` varchar(2) DEFAULT NULL,
  `ds_cidade` varchar(30) DEFAULT NULL,
  `cd_empresa` int(9) NOT NULL,
  `dt_inativacao` date DEFAULT NULL,
  PRIMARY KEY (`cd_filial`),
  KEY `fb_fil_emp` (`cd_empresa`),
  CONSTRAINT `fk_fil_emp` FOREIGN KEY (`cd_empresa`) REFERENCES `tbl_emp` (`cd_emp`)
) ENGINE=InnoDB AUTO_INCREMENT=20 DEFAULT CHARSET=utf8;

Utilizando essa nossa estrutura, cada filial está obrigatóriamente, vinculada à uma empresa. Sendo assim, uma empresa pode possuir nenhuma, uma ou muitas filiais. Na UML, esse tipo de relação é denominado dependência.

Caso às tabelas estejam modeladas com chaves estrangeiras, como às acima, se você utilizar o Gii, ele automaticamente já vai criar as relations. Caso contrário, você terá que defini-las “programaticamente”. Seguindo a seguinde estrutura:

return array(  
    'nome_do_relacionamento' => array(self::TIPO_DO_RELACIONAMENTO, 'Modelo', 'coluna_da_tabela'),
);

Observação: Lembre-se sempre que o nome do relacionamento não pode ser o nome de uma coluna.

Logo, em nosso exemplo teremos os seguintes relacionamentos, em seus respectivos modelos:

O modelo empresa utilizando o relacionamento HASMANY, para o modelo Filial especificamente na coluna cdempresa.
model/Empresa.php

public function relations()  
{
    return array(
        'filiais' => array(self::HAS_MANY, 'Filial', 'cd_empresa'),
    );
}

O modelo Filial utilizando o relacionamento BELONGSTO, para o modelo Empresa, mas, utilizando a própria coluna cdempresa.
model/Filial.php

public function relations()  
{
    return array(
        'empresa' => array(self::BELONGS_TO, 'Empresa', 'cd_empresa'),
    );
}

Por enquanto é só, no próximo post vou explicar como resgatar esses valores.

Caso você queira baixar o exemplo como está clique aqui, o banco está dentro da pasta data, não se esqueça de alterar as configurações no main.php

Yii – Wform Unexpected T_PAAMAYIM_NEKUDOTAYIM

Se alguém já utilizou a extensão wForm, talvez você se dapare com o erro acima, porém, só se você estiver utilizando uma versão inferior à 5.3 do php.

Utilizando a palavra static no manual do php:

A partir do PHP 5.3.0, é possível referenciar a classe usando uma variável. O valor da variável não pode ser uma palavra-chave (eg self, parent e static).

A razão para o erro é simplesmente porque a sintaxe não é suportada em versões menores do php do que à 5.3.

Você pode alterar a extensão para utilizar ReflectionClass.

<?php  
    class Teste {
        const UM = "Número UM";
        const DOIS = "Número DOIS";
    }

    $obj = new ReflectionClass("Teste");
    echo $obj->getconstant("UM")."\n";
    echo $obj->getconstant("DOIS")."\n";

?>

Saída (output):

Número UM
Número DOIS

Você também poderia escrever um método que retorna o valor correto, sem a necessidade de criar uma instância do objeto, e, por ser genérico, pode ser facilmente copiado e colado para as constantes

<?php  
class TESTE {  
    var $CONSTANTE = 1;
    function CONSTANTE() { $vars = get_class_vars(__CLASS__); return $vars[strToUpper(__FUNCTION__)]; }
}

echo TESTE::CONSTANTE();  
?>

Mas com certeza a alternativa mais rápida e indolor, é – se possível – atualizar o php para 5.3, pelo menos.

Yii – It is not safe to rely on the system’s timezone settings

Quando upamos um sistema novo para um determinado servidor, apareceu o seguinte warning.

getdate() [function.getdate]: It is not safe to rely on the system’s timezone settings. You are required to use the date.timezone setting or the datedefaulttimezoneset() function. In case you used any of those methods and you are still getting this warning, you most likely misspelled the timezone identifier. We selected ‘America/SaoPaulo’ for ‘BRT/-3.0/no DST’ instead

Esse warning aparece geralmente quando temos em nosso php.ini o error level atribuído como strict.

Para resolver abra o index.php da raiz do seu sistema.

e troque de:

Yii::createWebApplication($config)->run();  

Para:

$app = Yii::createWebApplication($config);
Yii::app()->setTimeZone("America/Sao_Paulo");  
$app->run();

© 2017 Adler Dias

Theme by Anders NorénUp ↑