Como criar um CMS completo com CakePHP – Parte 6 – Gerenciamento de páginas com SEO, gerenciamento do menu, CKeditor e upload de imagens integrado Artigo

Conheça os cursos gratuitos do WebDevBr! - Inscreva-se!


Este artigo foi publicado a 4 anos, 2 meses, 2 semanas, 4 dias atrás.

Bem, estamos na reta final do nosso CMS, neste artigo quero mostrar como fazer o sistema de gerenciamento das páginas do site, alguns recursos são de vital importância para um CMS e estão diretamente relacionados a esta seção do site, como gerenciamento de SEO (tag description, tag title, keywords (embora hoje em dia não sejam tão relevantes, faz parte da função do programador não limitar sua ferramenta e permitir uma maior abrangência a todos os casos, dentro dos limites da possibilidade, claro) e  URL da página). Outra coisa que também tem que estar presente são as opções de visibilidade, nem sempre uma página tem q estar visível no menu ou mesmo ativada no site e isso tem que ser feito.

Vou usar a seguinte estrutura no banco de dados:

id - Identificação única da página
titulo - Título principal e do menu
corpo - O conteúdo
title - O que vai na tag title
descricao - A Descrição para motores de busca
tags - Palavras-chave
slug - A URL da página
menu - Se estará visível no menu
habilitar - Se a página está visível no site
parent_id - Para o Tree Behavior
lft - Para o Tree Behavior
rght - Para o Tree Behavior
created - Data de criação
modified - Data de modificação

Gostou deste artigo?

Receba atualizações semanais com novos artigos do WebDevBr e outras dicas!

Se você observar os campos parent_id, lft e rght vai notar que as descrições dizem que serão usados pelo Tree Behavior, este behavior serve para ordenar itens em esquema de itens e subitens (tree de árvore, não confunda com three, que é 3 em inglês), é muito interessante para ordenar menus de sites por exemplo.

Se você observar o nosso schema.php, ele não está dessa forma, por isso vamos alterar usando o Migration e criar versões do nosso banco de dados.

Schema e Migration

O Migration é um recurso que permite versionamento do banco de dados, ou seja, versão 1, versão 2, versão 3, e você pode ir e voltar para qualquer versão que quiser, é um recurso muito importante a medida que o CMS for evoluindo, por enquanto, encare isso de forma didática.

No console do seu sistema operacional, acesse a pasta app do seu projeto e rode o comando para gerar um schema.php novo, assim:

Console/cake schema generate

Novamente reforço que o windows usa barra invertida.

Ele fará uma pergunta com três respostas possíveis, Overwrite (sobrescrever), Snapshot (Instantâneo) e Quit (Desistir), tecle S para escolher Snapshot, ele irá criar um novo arquivo chamado schema_1.php.

Abra o schema.php e copie o conteúdo para o schema_1.php, na linha 83 está abrindo a variável $páginas, vamos alterar toda ela para:

public $paginas = array(
    'id' => array('type' => 'integer', 'null' => false, 'default' => null, 'key' => 'primary'),
    'titulo' => array('type' => 'string', 'null' => false, 'default' => null, 'length' => 512, 'collate' => 'latin1_swedish_ci', 'charset' => 'latin1'),
    'corpo' => array('type' => 'text', 'null' => false, 'default' => null, 'collate' => 'latin1_swedish_ci', 'charset' => 'latin1'),
    'title' => array('type' => 'string', 'null' => false, 'default' => null, 'length' => 512, 'collate' => 'latin1_swedish_ci', 'charset' => 'latin1'),
    'descricao' => array('type' => 'text', 'null' => false, 'default' => null, 'collate' => 'latin1_swedish_ci', 'charset' => 'latin1'),
    'tags' => array('type' => 'string', 'null' => false, 'default' => null, 'length' => 512, 'collate' => 'latin1_swedish_ci', 'charset' => 'latin1'),
    'slug' => array('type' => 'string', 'null' => false, 'default' => null, 'length' => 512, 'collate' => 'latin1_swedish_ci', 'charset' => 'latin1'),
    'menu' => array('type' => 'integer', 'null' => false, 'default' => null, 'length' => 1),
    'habilitar' => array('type' => 'integer', 'null' => false, 'default' => null, 'length' => 1),
    'parent_id' => array('type' => 'integer', 'null' => true, 'default' => null),
    'lft' => array('type' => 'integer', 'null' => true, 'default' => null),
    'rght' => array('type' => 'integer', 'null' => true, 'default' => null),
    'created' => array('type' => 'datetime', 'null' => false, 'default' => null),
    'modified' => array('type' => 'datetime', 'null' => false, 'default' => null),
    'indexes' => array(
        'PRIMARY' => array('column' => 'id', 'unique' => 1)
    ),
    'tableParameters' => array('charset' => 'latin1', 'collate' => 'latin1_swedish_ci', 'engine' => 'MyISAM')
);

E pronto, salve e feche, agora que você tem duas versões do seu banco de dados, basta usar o console para alternar entre eles com:

Console/cake schema update -s 1

Aonde o 1 é a versão do schema que você quer, omitindo o -s 1 você seta o schema.php.

Rodando este código seu banco de dados já estará pronto, porém o instalador não, o atual apenas reconhece o schema.php, então atualize seu instalador para este, apenas descompacte na pasta plugins, substituindo o atual.

Ok, banco atualizado e trabalhando com Schema e Migration.

O Model

O trabalho de manipular dados e fazer validações é do model por isso vamos fazer o nosso bem rico e facilitar o máximo possível para seu cliente, para isso vamos fazer com que ao preencher o título e omitir title e slug eles sejam gerados automaticamente, mas o slug tem que ser minusculo e com traço ou underline ao invés de espaços. Também vou informar uma validação de dados para que ele não esqueça o título e nem escreva algo que já existe, nosso model fica assim:

<?php

class Pagina extends AppModel{
     //habilita o TreeBehavior que será usado no menu
    public $actsAs = array('Tree');

    //faz com que o título seja obrigatório e único
    public $validate = array(
         'titulo' => array(
            'unico' => array(
                'rule'     => 'isUnique',
                'message'  => 'Já existe uma página com este nome, escolha outro.'
            ),
    'obrigatorio'=>array(
        'rule' => 'notEmpty',
        'message'  => 'O título da página não pode ficar em branco.'
    )
        )
    );

    //antes de salvar:
    public function beforeSave($options = array()) {
        //verifica se o título foi enviado
        if(isset($this->data['Pagina']['titulo'])){
            //e seta o title igual, caso  esteja vazio
            if (!isset($this->data['Pagina']['title'])||empty($this->data['Pagina']['title']))
                $this->data['Pagina']['title']=$this->data['Pagina']['titulo'];

            //caso o slug esteja veio usa o titulo
            if (!isset($this->data['Pagina']['slug'])||empty($this->data['Pagina']['slug'])){
                //e usa o inflector pra fazer o slug com traço, troque o '-' por underline se qusier.
                $this->data['Pagina']['slug']=Inflector::slug(strtolower($this->data['Pagina']['titulo']),'-');
            }else{
                //faz o mesmo, mas com o slug se ele tiver sido enviado
                $this->data['Pagina']['slug']=Inflector::slug(strtolower($this->data['Pagina']['slug']),'-');
            }
            //aqui, caso não exista o campo menu, ele é setado para 1
            if (!isset($this->data['Pagina']['menu']))
                $this->data['Pagina']['menu']=1;
            //aqui, caso não exista o campo hamilitar, ele é setado para 1
            if (!isset($this->data['Pagina']['habilitar']))
                $this->data['Pagina']['habilitar']=1;
        }

        //e se você alterar isso pra false, não salva, hehehe
        return true;
    }
}

O código está bem comentado, não vou entrar em detalhes, bora pro Controller.

Controller

O controller terá as seguintes actions:

display - Mostra a página
menu - Retorna o menu
admin_index - Lista as páginas na administração
admin_add - Adiciona uma nova administração
admin_edita - Edita uma página
admin_sobe - Move a página uma posição antes
admin_desce - Move a página uma posição depois
admin_remove - Remove a página

Além das actions, teremos também o beforeRender e o beforeFilter, no beforeFilter iremos fazer uma manipulação interessante de actions, quando a action ajax_display for chamada, iremos avisar para usar a display e na display setar a view display.ctp com isso teremos uma view, uma action para quando for setado o prefixo ajax, ou sem prefixo nenhum.

Lembra que criei uma action só para cadastro e edição nos usuários, aqui vou usar duas, porque quero que o CMS crie todo o básico para o usuário com ele apenas escrevendo o título da página.

E ficará assim:

<?php

class PaginasController extends AppController {
     function beforeFilter(){
        parent::beforeFilter();
        if($this->action=='ajax_display')$this->action='display';
        $this->Auth->allow(array('display','menu'));
    }

    function beforeRender(){
        parent::beforeRender();
        $this->set('active','paginas');
    }

    //Lista as páginas na adiminstração
    function admin_index(){
        $retorno =$this->Pagina->find('all',array('order'=>'lft'));
        $this->set('retorno',$retorno);
    }

    //mostra as páginas do site
    function display($slug=null){
        $conteudo=$this->Pagina->find('first',array('conditions'=>array('slug'=>$slug),'fields'=>array('titulo','title','descricao','tags','slug','corpo')));
        if(count($conteudo)==0)throw new NotFoundException(__('Ops! Página não encontrada'));
        $conteudo = $conteudo['Pagina'];
        $seo['title']=$conteudo['title'];
        $seo['tags']=$conteudo['tags'];
        $seo['descricao']=$conteudo['descricao'];
        $this->set('conteudo',$conteudo);
        $this->set('seo',$seo);
        $this->render('display');
    }

    function menu(){
        if($this->request->is('requested')){
            $menu=$this->Pagina->find('all',array(
                'conditions'=>array(
                    'menu'=>1,
                    'habilitar'=>1
                ),
                'order'=>'lft'
            ));
            return $menu;
        }
    }
    function admin_add(){
        if($this->request->is('post')||$this->request->is('put')):
            $this->Pagina->create();
            if($this->Pagina->save($this->request->data)){
                $this->Session->setFlash(__('Página salva com sucesso!'),'sucesso');
                return $this->redirect(array('action'=>'index'));
                return $this->redirect(array('action'=>'index'));
            }
                $this->Session->setFlash(__('Página não pode ser salva!'),'erro');
        endif;
    }
    function admin_edita($id=null){
        $this->Pagina->id=$id;
        if(!$this->Pagina->exists())throw new NotFoundException(__('Página não encontrada'));
        if($this->request->is('post')||$this->request->is('put')):
            if($this->Pagina->save($this->request->data)){
                $this->Session->setFlash(__('Página salva com sucesso!'),'sucesso');
            }
                $this->Session->setFlash(__('Página não pode ser salva!'),'erro');
        else:
            $this->request->data=$this->Pagina->read();
        endif;
    }
    function admin_sobe($id=null){
        $this->Pagina->id=$id;
        if(!$this->Pagina->exists())throw new NotFoundException(__('Página não encontrada'));
        $this->Pagina->moveUp($id,abs(1));
        $this->Session->setFlash(__('Posição da página alterada com sucesso!'),'sucesso');
        return $this->redirect(array('action'=>'index'));
    }
    function admin_desce($id=null){
        $this->Pagina->id=$id;
        if(!$this->Pagina->exists())throw new NotFoundException(__('Página não encontrada'));
        $this->Pagina->moveDown($id,abs(1));
        $this->Session->setFlash(__('Posição da página alterada com sucesso!'),'sucesso');
        return $this->redirect(array('action'=>'index'));
    }
    function admin_remove($id=null){
        $this->Pagina->id = $id;
        if(!$this->Pagina->exists())throw new NotFoundException('Página inexistente');
        if($this->Pagina->delete()):
            $this->Session->setFlash(__('Página removida com sucesso!'),'sucesso');
            return $this->redirect(array('action'=>'index'));
        endif;
        $this->Session->setFlash(__('Página não pode ser removida!'),'erro');
        return $this->redirect(array('action'=>'index'));
    }
}

Note as actions admin_sobe e admin_desce possuem $this->Pagina->moveUp($id,abs(1)) e $this->Pagina->moveDown($id,abs(1)), ambas alteram automaticamente as posições da página em 1 (o abs(1)), é um recurso da Tree Behavior muito simples de se usar.

As Views

Para fechar, vou passar as views que serão 4, já que as actions admin_sobe, admin_desce e admin_remove não terão uma versão para se ver, serão redirecionadas e apenas ações serão executadas e a action menu será usada por um element, as funções são básicas e vou deixar para você estudar, note apenas que a admin_index não tem paginação pelo CakePHP e sim um javascript que executa tal função, e ainda adiciona ordenação e pesquisa ajax na lista, um poquinho de frontend pra ajudar.

Baixe aqui as views.

O Editor de conteúdo (CKeditor + Upload de imagens multiplo)

Pra completar essa parte vou aproveitar pra configurar o editor com upload de imagem, basta baixar e descompactar na pasta de Downloads, se quando você adicionou o Plugin de Instalação já deixou o loadAll no bootstrap.php(app/Config/bootstrap.php) não precisa fazer mais nada no backend, caso contrário terá que carregar lá com o Instalador.

Aqui o Plugin.

Essa versão que segue no plugin precisa de uma variável javascript chamada base_url que estou passando no admin_edita, ok.

Se quiser mais detalhes de como funciona, veja aqui o artigo sobre o CKeditor e o upload de imagens.

O Roteamento

E por fim, vamos corrigir nosso routes.php, abra em app/Config/routes.php e aonde está:

Router::connect('/', array('controller' =&gt; 'pages', 'action' =&gt; 'display', 'home'));

Troque o pages por paginas, assim:

Router::connect('/', array('controller' =&gt; 'paginas', 'action' =&gt; 'display', 'home'));

E depois de CakePlugin::routes(); adicione:

Router::connect('/:slug', array('controller' => 'paginas ', 'action' => 'display'),array('pass'=>array('slug')));

Com isso o Routes já está configurado.

Próximo artigo

Nosso CMS já gerencia e exibe páginas corretamente, claro que ainda precisamos definir alguns detalhes, como a exibição das tags de SEO e vamos fazer isso no próximo artigo, quando vamos preparar os elements usados no tema do site, não percam esse artigo vamos criar também o menu com base nas configurações de visibilidade e um helper pra ajudar, ok.

Então este foi 6º artigo de 9 que ensina como construir um CMS completo com CakePHP.


Cursos relacionados


* Parcelamento apenas cartão de crédito! Pode haver uma pequena variação no parcelamento em relação a simulações apresentadas!