Para terminar nossa aplicação com estrutura MVC em PHP, vamos criar um sistema de cadastro de notícias bem simples, com campos de título, data, autor, texto e URL da imagem.

Além disso, criaremos uma área administrativa para gerenciar as notícias, onde poderemos criar, apagar, editar e listar notícias cadastradas.

Este é apenas um exemplo sobre como iremos utilizar nossa aplicação MVC em PHP. A partir desse tutorial você estará apto a criar seus próprios modelos, controladores e views simplesmente seguindo o mesmo modelo.

Download

Antes de qualquer coisa, faça o download da aplicação e acompanhe em seu computador. Segue o link para download:

Caso queira contribuir com o projeto, acesso no Github:

Se tiver perdido alguma parte desse tutorial, deixo os links para facilitar sua vida:

Então vamos codificar um pouco…

Criando o controller para as notícias

Primeiramente vamos criar o nosso controller, portando acesse a pasta “controllers” e crie um arquivo chamado “noticias-controller.php”.

Neste controller teremos que ter duas ações:

  • index (pública) – Lista as notícias na página e não precisa de usuário e senha para o acesso;
  • adm (restrita) – Gerenciamento das notícias – CRUD com PDO

Veja o código do controller:

<?php
/**
 * NoticiasController - Controller de exemplo
 *
 * @package TutsupMVC
 * @since 0.1
 */
class NoticiasController extends MainController
{

   /**
    * $login_required
    *
    * Se a página precisa de login
    *
    * @access public
    */
    public $login_required = false;

   /**
    * $permission_required
    *
    * Permissão necessária
    *
    * @access public
    */
    public $permission_required;

   /**
    * INDEX
    *
    * Carrega a página "/views/noticias/index.php"
    */
    public function index() {
	// Título da página
	$this->title = 'Notícias';
	
	// Carrega o modelo para este view
        $modelo = $this->load_model('noticias/noticias-adm-model');
				
	/** Carrega os arquivos do view **/
		
	// /views/_includes/header.php
        require ABSPATH . '/views/_includes/header.php';
		
	// /views/_includes/menu.php
        require ABSPATH . '/views/_includes/menu.php';
		
	// /views/noticias/index.php
        require ABSPATH . '/views/noticias/noticias-view.php';
		
	// /views/_includes/footer.php
        require ABSPATH . '/views/_includes/footer.php';
		
    } // index
	
   /**
    * ADM
    *
    * Carrega a página "/views/noticias/noticias-adm-view.php"
    */
    public function adm() {
		// Page title
		$this->title = 'Gerenciar notícias';
		$this->permission_required = 'gerenciar-noticias';
		
		// Verifica se o usuário está logado
		if ( ! $this->logged_in ) {
		
			// Se não; garante o logout
			$this->logout();
			
			// Redireciona para a página de login
			$this->goto_login();
			
			// Garante que o script não vai passar daqui
			return;
		
		}
		
		// Verifica se o usuário tem a permissão para acessar essa página
		if (!$this->check_permissions($this->permission_required, $this->userdata['user_permissions'])) {
		
			// Exibe uma mensagem
			echo 'Você não tem permissões para acessar essa página.';
			
			// Finaliza aqui
			return;
		}
	
	// Carrega o modelo para este view
        $modelo = $this->load_model('noticias/noticias-adm-model');
		
	/** Carrega os arquivos do view **/
		
	// /views/_includes/header.php
        require ABSPATH . '/views/_includes/header.php';
		
	// /views/_includes/menu.php
        require ABSPATH . '/views/_includes/menu.php';
		
	// /views/noticias/index.php
        require ABSPATH . '/views/noticias/noticias-adm-view.php';
		
	// /views/_includes/footer.php
        require ABSPATH . '/views/_includes/footer.php';
		
    } // adm
	
} // class NoticiasController

Veja que temos duas ações agora, index e adm. Ambas serão acessadas pelas URLs:

  • index: dominio.com/noticias/
  • adm: dominio.com/noticias/adm/

Agora vamos criar o modelo para as notícias.

Criando o model para as notícias

Nosso model será compartilhado pelos dois views que teremos, um para a ação “adm” outro para “index”.

Lembre-se: Modelos ficam dentro da pasta models separados por pastas. Nosso model estará dentro da pasta noticias com o nome de noticias-adm-model.php.

Veja seu código:

<?php 
/**
 * Modelo para gerenciar notícias
 *
 * @package TutsupMVC
 * @since 0.1
 */
class NoticiasAdmModel extends MainModel
{

	/**
	 * $posts_per_page
	 *
	 * Receberá o número de posts por página para configurar a listagem de 
	 * notícias. Também utilizada na paginação. 
	 *
	 * @access public
	 */
	public $posts_por_pagina = 5;
	
	/**
	 * Construtor para essa classe
	 *
	 * Configura o DB, o controlador, os parâmetros e dados do usuário.
	 *
	 * @since 0.1
	 * @access public
	 * @param object $db Objeto da nossa conexão PDO
	 * @param object $controller Objeto do controlador
	 */
	public function __construct( $db = false, $controller = null ) {
		// Configura o DB (PDO)
		$this->db = $db;
		
		// Configura o controlador
		$this->controller = $controller;

		// Configura os parâmetros
		$this->parametros = $this->controller->parametros;

		// Configura os dados do usuário
		$this->userdata = $this->controller->userdata;
	}
	
	/**
	 * Lista notícias
	 *
	 * @since 0.1
	 * @access public
	 * @return array Os dados da base de dados
	 */
	public function listar_noticias () {
	
		// Configura as variáveis que vamos utilizar
		$id = $where = $query_limit = null;
		
		// Verifica se um parâmetro foi enviado para carregar uma notícia
		if ( is_numeric( chk_array( $this->parametros, 0 ) ) ) {
			
			// Configura o ID para enviar para a consulta
			$id = array ( chk_array( $this->parametros, 0 ) );
			
			// Configura a cláusula where da consulta
			$where = " WHERE noticia_id = ? ";
		}
		
		// Configura a página a ser exibida
		$pagina = ! empty( $this->parametros[1] ) ? $this->parametros[1] : 1;
		
		// A paginação inicia do 0
		$pagina--;
		
		// Configura o número de posts por página
		$posts_por_pagina = $this->posts_por_pagina;
		
		// O offset dos posts da consulta
		$offset = $pagina * $posts_por_pagina;
		
		/* 
		Esta propriedade foi configurada no noticias-adm-model.php para
		prevenir limite ou paginação na administração.
		*/
		if ( empty ( $this->sem_limite ) ) {
		
			// Configura o limite da consulta
			$query_limit = " LIMIT $offset,$posts_por_pagina ";
		
		}
		
		// Faz a consulta
		$query = $this->db->query(
			'SELECT * FROM noticias ' . $where . ' ORDER BY noticia_id DESC' . $query_limit,
			$id
		);
		
		// Retorna
		return $query->fetchAll();
	} // listar_noticias
	
	/**
	 * Obtém a notícia e atualiza os dados se algo for postado
	 *
	 * Obtém apenas uma notícia da base de dados para preencher o formulário de
	 * edição.
	 * Configura a propriedade $this->form_data.
	 *
	 * @since 0.1
	 * @access public
	 */
	public function obtem_noticia () {
		
		// Verifica se o primeiro parâmetro é "edit"
		if ( chk_array( $this->parametros, 0 ) != 'edit' ) {
			return;
		}
		
		// Verifica se o segundo parâmetro é um número
		if ( ! is_numeric( chk_array( $this->parametros, 1 ) ) ) {
			return;
		}
		
		// Configura o ID da notícia
		$noticia_id = chk_array( $this->parametros, 1 );
		
		/* 
		Verifica se algo foi postado e se está vindo do form que tem o campo
		insere_noticia.
		
		Se verdadeiro, atualiza os dados conforme a requisição.
		*/
		if ( 'POST' == $_SERVER['REQUEST_METHOD'] && ! empty( $_POST['insere_noticia'] ) ) {
		
			// Remove o campo insere_notica para não gerar problema com o PDO
			unset($_POST['insere_noticia']);
			
			// Verifica se a data foi enviada
			$data = chk_array( $_POST, 'noticia_data' );
			
			/*
			Inverte a data para os formatos dd-mm-aaaa hh:mm:ss
			ou aaaa-mm-dd hh:mm:ss
			*/
			$nova_data = $this->inverte_data( $data );
			
			// Adiciona a data no $_POST		
			$_POST['noticia_data'] = $nova_data;
			
			// Tenta enviar a imagem
			$imagem = $this->upload_imagem();
			
			// Verifica se a imagem foi enviada
			if ( $imagem ) {
				// Adiciona a imagem no $_POST
				$_POST['noticia_imagem'] = $imagem;
			}
			
			// Atualiza os dados
			$query = $this->db->update('noticias', 'noticia_id', $noticia_id, $_POST);
			
			// Verifica a consulta
			if ( $query ) {
				// Retorna uma mensagem
				$this->form_msg = '<p class="success">Notícia atualizada com sucesso!</p>';
			}
			
		}
		
		// Faz a consulta para obter o valor
		$query = $this->db->query(
			'SELECT * FROM noticias WHERE noticia_id = ? LIMIT 1',
			array( $noticia_id )
		);
		
		// Obtém os dados
		$fetch_data = $query->fetch();
		
		// Se os dados estiverem nulos, não faz nada
		if ( empty( $fetch_data ) ) {
			return;
		}
		
		// Configura os dados do formulário
		$this->form_data = $fetch_data;
		
	} // obtem_noticia
	
	/**
	 * Insere notícias
	 *
	 * @since 0.1
	 * @access public
	 */
	public function insere_noticia() {
	
		/* 
		Verifica se algo foi postado e se está vindo do form que tem o campo
		insere_noticia.
		*/
		if ( 'POST' != $_SERVER['REQUEST_METHOD'] || empty( $_POST['insere_noticia'] ) ) {
			return;
		}
		
		/*
		Para evitar conflitos apenas inserimos valores se o parâmetro edit
		não estiver configurado.
		*/
		if ( chk_array( $this->parametros, 0 ) == 'edit' ) {
			return;
		}
		
		// Só pra garantir que não estamos atualizando nada
		if ( is_numeric( chk_array( $this->parametros, 1 ) ) ) {
			return;
		}
			
		// Tenta enviar a imagem
		$imagem = $this->upload_imagem();
		
		// Verifica se a imagem foi enviada
		if ( ! $imagem ) {
			return;		
		}
		
		// Remove o campo insere_notica para não gerar problema com o PDO
		unset($_POST['insere_noticia']);
		
		// Insere a imagem em $_POST
		$_POST['noticia_imagem'] = $imagem;
		
		// Configura a data
		$data = chk_array( $_POST, 'noticia_data' );
		$nova_data = $this->inverte_data( $data );
					
		// Adiciona a data no POST
		$_POST['noticia_data'] = $nova_data;
		
		// Insere os dados na base de dados
		$query = $this->db->insert( 'noticias', $_POST );
		
		// Verifica a consulta
		if ( $query ) {
		
			// Retorna uma mensagem
			$this->form_msg = '<p class="success">Notícia atualizada com sucesso!</p>';
			return;
			
		} 
		
		// :(
		$this->form_msg = '<p class="error">Erro ao enviar dados!</p>';

	} // insere_noticia
	
	/**
	 * Apaga a notícia
	 *
	 * @since 0.1
	 * @access public
	 */
	public function apaga_noticia () {
		
		// O parâmetro del deverá ser enviado
		if ( chk_array( $this->parametros, 0 ) != 'del' ) {
			return;
		}
		
		// O segundo parâmetro deverá ser um ID numérico
		if ( ! is_numeric( chk_array( $this->parametros, 1 ) ) ) {
			return;
		}
		
		// Para excluir, o terceiro parâmetro deverá ser "confirma"
		if ( chk_array( $this->parametros, 2 ) != 'confirma' ) {
		
			// Configura uma mensagem de confirmação para o usuário
			$mensagem  = '<p class="alert">Tem certeza que deseja apgar a notícia?</p>';
			$mensagem .= '<p><a href="' . $_SERVER['REQUEST_URI'] . '/confirma/">Sim</a> | ';
			$mensagem .= '<a href="' . HOME_URI . '/noticias/adm/">Não</a></p>';
			
			// Retorna a mensagem e não excluir
			return $mensagem;
		}
		
		// Configura o ID da notícia
		$noticia_id = (int)chk_array( $this->parametros, 1 );
		
		// Executa a consulta
		$query = $this->db->delete( 'noticias', 'noticia_id', $noticia_id );
		
		// Redireciona para a página de administração de notícias
		echo '<meta http-equiv="Refresh" content="0; url=' . HOME_URI . '/noticias/adm/">';
		echo '<script type="text/javascript">window.location.href = "' . HOME_URI . '/noticias/adm/";</script>';
		
	} // apaga_noticia
	
	/**
	 * Envia a imagem
	 *
	 * @since 0.1
	 * @access public
	 */
	public function upload_imagem() {
	
		// Verifica se o arquivo da imagem existe
		if ( empty( $_FILES['noticia_imagem'] ) ) {
			return;
		}
		
		// Configura os dados da imagem
		$imagem         = $_FILES['noticia_imagem'];
		
		// Nome e extensão
		$nome_imagem    = strtolower( $imagem['name'] );
		$ext_imagem     = explode( '.', $nome_imagem );
		$ext_imagem     = end( $ext_imagem );
		$nome_imagem    = preg_replace( '/[^a-zA-Z0-9]/', '', $nome_imagem);
		$nome_imagem   .= '_' . mt_rand() . '.' . $ext_imagem;
		
		// Tipo, nome temporário, erro e tamanho
		$tipo_imagem    = $imagem['type'];
		$tmp_imagem     = $imagem['tmp_name'];
		$erro_imagem    = $imagem['error'];
		$tamanho_imagem = $imagem['size'];
		
		// Os mime types permitidos
		$permitir_tipos  = array(
			'image/bmp',
			'image/x-windows-bmp',
			'image/gif',
			'image/jpeg',
			'image/pjpeg',
			'image/png',
		);
		
		// Verifica se o mimetype enviado é permitido
		if ( ! in_array( $tipo_imagem, $permitir_tipos ) ) {
			// Retorna uma mensagem
			$this->form_msg = '<p class="error">Você deve enviar uma imagem.</p>';
			return;
		}
		
		// Tenta mover o arquivo enviado
		if ( ! move_uploaded_file( $tmp_imagem, UP_ABSPATH . '/' . $nome_imagem ) ) {
			// Retorna uma mensagem
			$this->form_msg = '<p class="error">Erro ao enviar imagem.</p>';
			return;
		}
		
		// Retorna o nome da imagem
		return $nome_imagem;
		
	} // upload_imagem
	
	/**
	 * Paginação
	 *
	 * @since 0.1
	 * @access public
	 */
	public function paginacao () {
	
		/* 
		Verifica se o primeiro parâmetro não é um número. Se for é um single
		e não precisa de paginação.
		*/
		if ( is_numeric( chk_array( $this->parametros, 0) ) ) {	
			return;
		}
		
		// Obtém o número total de notícias da base de dados
		$query = $this->db->query(
			'SELECT COUNT(*) as total FROM noticias '
		);
		$total = $query->fetch();
		$total = $total['total'];
		
		// Configura o caminho para a paginação
		$caminho_noticias = HOME_URI . '/noticias/index/page/';
		
		// Itens por página
		$posts_per_page = $this->posts_por_pagina;
		
		// Obtém a última página possível
		$last = ceil($total/$posts_per_page);
		
		// Configura a primeira página
		$first = 1;
		
		// Configura os offsets
		$offset1 = 3;
		$offset2 = 6;
		
		// Página atual
		$current = $this->parametros[1] ? $this->parametros[1] : 1;
		
		// Exibe a primeira página e reticências no início
		if ( $current > 4 ) {
			echo "<a href='$caminho_noticias$first'>$first</a> ... ";
		}
		
		// O primeiro loop toma conta da parte esquerda dos números
		for ( $i = ( $current - $offset1 ); $i < $current; $i++ ) {
			if ( $i > 0 ) {
				echo "<a href='$caminho_noticias$i'>$i</a>";
				
				// Diminiu o offset do segundo loop
				$offset2--;
			}
		}
		
		// O segundo loop toma conta da parte direita dos números
		// Obs.: A primeira expressão realmente não é necessária
		for ( ; $i < $current + $offset2; $i++ ) {
			if ( $i <= $last ) {
				echo "<a href='$caminho_noticias$i'>$i</a>";
			}
		}
		
		// Exibe reticências e a última página no final
		if ( $current <= ( $last - $offset1 ) ) {
			echo " ... <a href='$caminho_noticias$last'>$last</a>";
		}

	} // paginacao
	
} // NoticiasAdmModel

Este é um model bem grande, mas ele fará tudo o que precisamos.

Seus métodos fazem o seguinte:

  • listar_noticias() – Retorna um array com todas as notícias;
  • obtem_noticia() – Obtém uma única notícia. Também atualiza os dados caso algo seja enviado;
  • insere_noticia() – Insere uma notícia na base de dados;
  • apaga_noticia() – Apaga uma notícia da base de dados;
  • upload_imagem() – Faz o upload de imagens se alguma for enviada;
  • paginacao() – Cria uma paginação para apresentar as notícias na página inicial de notícias;

Os métodos estão bem comentados, portanto, leia os comentários e se ficar com dúvidas, basta questionar.

Agora vamos criar os views.

Criando um view para mostrar notícias

Agora vamos criar um dos views que precisamos, o para a área administrativa.

Lembre-se: Views ficam dentro da pasta views separados por pastas. Nosso view estará dentro da pasta noticias com o nome de noticias-adm-view.php e noticias-view.php.

Veja seu o código do arquivo noticias-adm-view.php.

/views/noticias/noticias-adm-view.php

<?php 
// Evita acesso direto a este arquivo
if ( ! defined('ABSPATH')) exit;

// Configura as URLs
$adm_uri = HOME_URI . '/noticias/adm/';
$edit_uri = $adm_uri . 'edit/';
$delete_uri = $adm_uri . 'del/';

		
// Carrega o método para obter uma notícia
$modelo->obtem_noticia();

// Carrega o método para inserir uma notícia
$modelo->insere_noticia();

// Carrega o método para apagar a notícia
$modelo->form_confirma = $modelo->apaga_noticia();

// Remove o limite de valores da lista de notícias
$modelo->sem_limite = true;
?>

<div class="wrap">

	<?php 
	// Mensagem de configuração caso o usuário tente apagar algo
	echo $modelo->form_confirma;
	?>

	<!-- Formulário de edição das notícias -->
	<form method="post" action="" enctype="multipart/form-data">
		<table class="form-table">
			<tr>
				<td>
					Título: <br>
					<input type="text" name="noticia_titulo" value="<?php 
					echo htmlentities( chk_array( $modelo->form_data, 'noticia_titulo') );
					?>" />
				</td>
			</tr>
			<tr>
				<td>
					Imagem: <br>
					<input type="file" name="noticia_imagem" value="" />
				</td>
			</tr>
			<tr>
				<td>
					Data: <br>
					<input type="text" name="noticia_data" value="<?php 
					$data = chk_array( $modelo->form_data, 'noticia_data');
					if ( $data && $data != '0000-00-00 00:00:00' )
					echo date('d-m-Y H:i:s', strtotime( $data ) );
					?>" />
				</td>
			</tr>
			<tr>
				<td>
					Autor: <br>
					<input type="text" name="noticia_autor" value="<?php 
					echo htmlentities( $_SESSION['userdata']['user_name'] );
					?>" />
				</td>
			</tr>
			<tr>
				<td>
					Texto da notícia: <br>
					<textarea name="noticia_texto"><?php
					echo htmlentities( chk_array( $modelo->form_data, 'noticia_texto') );
					?></textarea>
				</td>
			</tr>
			<tr>
				<td colspan="2">
					<?php 
					// Mensagem de feedback para o usuário
					echo $modelo->form_msg;
					?>
					<input type="submit" value="Save" />
				</td>
			</tr>
		</table>
		
		<input type="hidden" name="insere_noticia" value="1" />
	</form>
	
	<!-- LISTA AS NOTICIAS -->
	<?php $lista = $modelo->listar_noticias(); ?>

	<table class="list-table">

		<?php foreach( $lista as $noticia ):?>
			
			<tr>
				<td><?php echo $noticia['noticia_titulo']?></td>
				<td>
					<a href="<?php echo $edit_uri . $noticia['noticia_id']?>">
						Editar
					</a> 
					
					<a href="<?php echo $delete_uri . $noticia['noticia_id']?>">
						Apagar
					</a>
				</td>
			</tr>
			
		<?php endforeach; ?>

	</table>

</div> <!-- .wrap -->

Este view faz todo o sistema de CRUD das notícias utilizando as ações do nosso model.

/views/noticias/noticias-view.php

Veja agora como apresentaremos os dados para o usuário final (este é o view da ação index):

<?php 
// Evita acesso direto a este arquivo
if ( ! defined('ABSPATH')) exit; 
?>

<div class="wrap">

<?php
// Número de posts por página
$modelo->posts_por_pagina = 10;

// Lista notícias
$lista = $modelo->listar_noticias(); 
?>

<?php foreach( $lista as $noticia ):?>
	
	<!-- Título -->
	<h1>
		<a href="<?php echo HOME_URI?>/noticias/index/<?php echo $noticia['noticia_id']?>">
			<?php echo $noticia['noticia_titulo']?>
		</a>
	</h1>

	<?php 
	// Verifica se estamos visualizando uma única notícia
	if ( is_numeric( chk_array( $modelo->parametros, 0 ) ) ): // single
	?>
	
		<p>
		<?php echo $modelo->inverte_data( $noticia['noticia_data'] );?> | 
		<?php echo $noticia['noticia_autor'];?> 
		</p>
		
		<p>
			<img src="<?php 
			echo HOME_URI . '/views/_uploads/' . $noticia['noticia_imagem']; ?>">
		</p>
		
		<?php echo $noticia['noticia_texto'];?>
		
	<?php endif;  // single ?>
	
<?php endforeach; ?>

<?php $modelo->paginacao();?>

</div> <!-- .wrap -->

Isso deverá gerar uma lista com as notícias, links para a página de uma notícia única e uma paginação em baixo.

Dentro da notícia outros campos serão apresentados, como imagem, autor, data e texto.

Veja um exemplo:

Cadastro de notícias

Cadastro de notícias

Campos preenchidos

Campos preenchidos

Notícia sendo visualizada

Notícia sendo visualizada

Lembre-se, isto é apenas um exemplo da funcionalidade do sistema, você deve criar seus próprios modelos, controles e views.

Download e outras partes do artigo

Segue o link para download:

Caso queira contribuir com o projeto, acesso no Github:

Se tiver perdido alguma parte desse tutorial, deixo os links para facilitar sua vida:

Em caso de dúvidas, questione!