Alura > Cursos de Programação > Cursos de PHP > Conteúdos de PHP > Primeiras aulas do curso PHP na Web: aplicando boas práticas e PSRs

PHP na Web: aplicando boas práticas e PSRs

Pensando em herança - Apresentação

Boas-vindas! Sou Vinicius Dias, serei seu instrutor ao longo deste curso sobre PHP na web com foco em boas práticas e PSRs.

Vinicius Dias é uma pessoa de pele clara e olhos escuros, com cabelos pretos e curtos. Usa bigode e cavanhaque e veste uma camiseta azul escura, escrito "caelum — ensino e inovação". Há um microfone de lapela na gola de sua camiseta. Ele está sentado em uma cadeira preta e, ao fundo, há uma parede lisa com iluminação azul clara.

O que vamos aprender?

Neste curso, daremos continuidade ao desenvolvimento do AluraPlay, nosso projeto web de armazenamento de vídeos do Youtube, em que é possível fazer upload de arquivos, editá-los e removê-los do banco de dados.

O foco deste curso será o ecossistema PHP e as boas práticas. Aprenderemos sobre uma forma melhorar de utilizar views e templates nos nossos controllers, dado que a estrutura atual do nosso projeto ainda não é ideal.

Vamos extrair códigos e implementar a funcionalidade de flash messages, que permitem o uso de mensagens de erro que duram apenas uma requisição.

Como em PHP não é possível ter herança múltipla, exploraremos uma alternativa para cenários de reutilização de código em diversas classes: as traits e o conceito de herança horizontal, incluindo resolução de conflitos de nomes.

A partir desse ponto em que já estudamos pontos específicos da linguagem PHP, partiremos para o ecossistema PHP. Vamos aprender sobre as PSR — PHP Standards Recommendations, isto é, as recomendações padrões do PHP.

Entre essas recomendações, estudaremos interfaces, guia de estilo de código e detalhes específicos de HTTP. Neste último ponto, exploraremos interfaces de requisição e resposta, e como acoplar nossos controllers a elas, seguindo padrões de mercado, implementados por diversos frameworks.

Além disso, vamos estudar a PSR-11, que foca em contêineres de injeção de dependências. Dessa forma, nosso projeto ficará bem mais profissional!

Por fim, aprenderemos sobre templating. Ao consultar o site PHP: The Right Way, descobriremos a recomendação do uso de templating em nossos sistemas, então, aprenderemos a utilizar uma biblioteca de templating para deixar nosso da view mais profissional.

Portanto, nesse projeto, já teremos algo muito próximo do que seria uma aplicação em PHP em produção, pois:

Comparando nosso projeto desde os cursos iniciais até agora, notaremos um grande avanço na nossa aplicação!

Vale ressaltar que este curso não ensinará tudo sobre PHP. Após este curso, é importante que você continue estudando sobre boas práticas, testes e frameworks!

Pensando em herança - Chamada do HTML

Como comentamos anteriormente, não adicionaremos muitas funcionalidades ao nosso projeto neste curso e focaremos mais em boas práticas. Então, começaremos implementando uma prática que nos ajudará a simplificar um trecho do nosso código.

No phpStorm, vamos acessar três arquivos de controllers:

Esses três controllers possuem um template HTML que é atualmente referenciado ao final do arquivo com o caminho completo. Em VideoFormController.php, por exemplo, temos a seguinte linha:

require_once __DIR__ . '/../../views/video-form.php';

O template é apenas o arquivo ao final desse caminho (como video-form.php) e o restante da linha é padronizado nos três controllers.

Além disso, há mais um ponto de atenção. Em VideoListController.php, quando realizamos o require_once do template video-list.php, temos acesso a todas as variáveis do escopo em que ele está incluído. No caso, trata-se apenas do $videoList, então não constitui um problema, mas haverá cenários em que mais informações ficarão à mostra.

Em VideoFormController.php, por exemplo, teremos acesso às variáveis $id, $video e quaisquer outros elementos declarados nesse escopo. Isso não é necessariamente um problema, contudo acabamos "vazando" um pouco de informação.

Vale lembrar que, em video-form.php, acabamos tendo acesso a todos os métodos (inclusive os privados) dos nossos controllers. Isso também não é necessariamente um problema, mas é interessante explorarmos como melhorar essa estrutura.

Herança

Vamos começar extraindo o código que realiza o require_once desses arquivos para uma função específica. Considerando que estamos trabalhando com três controllers diferentes, temos algumas opções de ter apenas um método acessível para todos.

Uma delas seria ter uma classe de auxílio em que colocaríamos o método que renderiza o HTML e, nos controllers, os receberíamos por meio de injeção de dependências.

Dado que se trata de três classes que são controllers com algum HTML, outra opção é utilizar a herança! Então, vamos adotar essa abordagem, a seguir.

Na pasta "src > Controller", vamos criar uma classe chamada ControllerWithHtml.php. Nela, desenvolveremos uma função chamada renderTemplate(). Por ora, ela não retornará nada:

<?php

declare(strict_types=1);

namespace Alura\Mvc\Controller;

class ControllerWithHtml
{
    public function renderTemplate(string $templateName): void
    {

    }
}

Vamos tornar esse método protected, assim todas as classes que estenderem ControllerWithHtml terão acesso a esse método, porém o restante do código não terá:

<?php

declare(strict_types=1);

namespace Alura\Mvc\Controller;

class ControllerWithHtml
{
    protected function renderTemplate(string $templateName): void
    {

    }
}

Em seguida, vamos fazer o require_once a partir do diretório atual, usando o caminho até a pasta "views", que corresponde ao trecho que se repete nos três controllers:

<?php

declare(strict_types=1);

namespace Alura\Mvc\Controller;

class ControllerWithHtml
{
    protected function renderTemplate(string $templateName): void
    {
      require_once __DIR__ . '/../../views/' . $templateName . '.php';
    }
}

Note que optamos pela extensão .php no final, mas podemos alterar esse trecho conforme nossa necessidade ou até mesmo passar o parâmetro já com a extensão.

Para melhorar ainda mais o código, podemos extrair o caminho dos templates para uma variável chamada $templatePath:

<?php

declare(strict_types=1);

namespace Alura\Mvc\Controller;

class ControllerWithHtml
{
    protected function renderTemplate(string $templateName): void
    {
      $templatePath = __DIR__ . '/../../views/';
      require_once $templatePath . $templateName . '.php';
    }
}

Outra opção seria adicionar o caminho como uma constante privada da nossa classe, chamada TEMPLATE_PATH, por exemplo. Como não se trata de uma constante global, será necessário acessá-la a partir da própria classe, utilizando a palavra-chave self:

<?php

declare(strict_types=1);

namespace Alura\Mvc\Controller;

class ControllerWithHtml
{
    private const TEMPLATE_PATH = __DIR__ . '/../../views/';
    protected function renderTemplate(string $templateName): void
    {
      require_once self::TEMPLATE_PATH . $templateName . '.php';
    }
}

Com esse código simples, conseguiremos remover os trechos repetidos dos controllers.

Página de login

Em LoginFormController.php, vamos definir que a classe LoginFormController estender ControllerWithHtml:

<?php
declare(strict_types=1);

namespace Alura\Mvc\Controller;

class LoginFormController extends ControllerWithHtml implements Controller
{
    public function processaRequisicao(): void
    {
        if (array_key_exists('logado', $_SESSION) && $_SESSION['logado'] === true) {
            header('Location: /');
            return;
        }
        require_once __DIR__ . '/../../views/login-form.php';
    }
}

E, em vez de fazer o require_once com o caminho completo, chamaremos o método renderTemplate(), passando o nome do template como parâmetro:

<?php
declare(strict_types=1);

namespace Alura\Mvc\Controller;

class LoginFormController extends ControllerWithHtml implements Controller
{
    public function processaRequisicao(): void
    {
        if (array_key_exists('logado', $_SESSION) && $_SESSION['logado'] === true) {
            header('Location: /');
            return;
        }

        $this->renderTemplate('login-form');
    }
}

Para nos certificar de que nosso sistema está funcionando, vamos abrir a interface do AluraPlay no navegador e acessar a página de login. Ao fazer o login, notamos que a aplicação segue funcionando normalmente.

Página de listagem

Em VideoListController.php, definiremos que a classe VideoListController estender ControllerWithHtml e substituiremos o require_once pela chamada do método renderTemplate():

<?php
declare(strict_types=1);
namespace Alura\Mvc\Controller;

use Alura\Mvc\Repository\VideoRepository;

class VideoListController extends ControllerWithHtml implements Controller
{
    public function __construct(private VideoRepository $videoRepository)
    {
    }

    public function processaRequisicao(): void
    {
        $videoList = $this->videoRepository->all();
        $this->renderTemplate('video-list');
    }
}

O phpStorn mostrará um aviso de que a variável $videoList não está mais sendo usada! Ao abrir a interface do AluraPlay na página de listagem, os vídeos não serão exibidos, pois não temos mais acesso à variável $videoList!

Como comentamos, anteriormente o template tinha acesso a todas as variáveis do escopo. Contudo, agora o escopo é o método renderTemplate(), que não tem nenhuma variável.

Como solução, vamos passar um segundo parâmetro para o método renderTemplate(). Esse parâmetro será um array associativo com o contexto do template, ou seja, os recursos disponíveis:

<?php

declare(strict_types=1);

namespace Alura\Mvc\Controller;

class ControllerWithHtml
{
    private const TEMPLATE_PATH = __DIR__ . '/../../views/';
    protected function renderTemplate(string $templateName, array $context): void
    {
      require_once self::TEMPLATE_PATH . $templateName . '.php';
    }
}

Esse contexto terá um formato semelhante ao seguinte:

$context = [
    'videoList' => [],
    'title' => 'Título'
]

Nosso objetivo é extrair cada uma das chaves desse array associativo como se fossem variáveis. Seguindo o exemplo, o resultado seria uma variável chamada $videoList com um array e outra variável chamada $title cujo valor é uma string.

Existe uma função em PHP que seleciona um array associativo e extrai todas as suas chaves, tornando-as variáveis — a função extract(). Portanto, vamos chamá-la e passar o contexto como parâmetro:

<?php

declare(strict_types=1);

namespace Alura\Mvc\Controller;

class ControllerWithHtml
{
    private const TEMPLATE_PATH = __DIR__ . '/../../views/';

    protected function renderTemplate(string $templateName, array $context): void
    {
        extract($context);
        require_once self::TEMPLATE_PATH . $templateName . '.php';
    }
}

Após chamar a função extract(), teremos acesso à variável $videoList, desde que haja uma chave videoList no contexto.

Caso você queira conhecer mais sobre a função extract(), você pode consultar a documentação oficial do PHP.

O segundo parâmetro será opcional, pois nem todo template precisará de uma variável, como é o caso da página de login. Portanto, vamos adicionar o valor padrão de array $context para torná-lo opcional:

<?php

declare(strict_types=1);

namespace Alura\Mvc\Controller;

class ControllerWithHtml
{
    private const TEMPLATE_PATH = __DIR__ . '/../../views/';

    protected function renderTemplate(string $templateName, array $context = []): void
    {
        extract($context);
        require_once self::TEMPLATE_PATH . $templateName . '.php';
    }
}

Dessa forma, não é preciso alterar nada em LoginFormController.php, dado que o segundo parâmetro é opcional. Já em VideoListController.php, passaremos o contexto na chamada de renderTemplate():

<?php
declare(strict_types=1);
namespace Alura\Mvc\Controller;

use Alura\Mvc\Repository\VideoRepository;

class VideoListController extends ControllerWithHtml implements Controller
{
    public function __construct(private VideoRepository $videoRepository)
    {
    }

    public function processaRequisicao(): void
    {
        $videoList = $this->videoRepository->all();
        $this->renderTemplate(
            'video-list',
            ['videoList' => $videoList]
        );
    }
}

Após salvar as alterações, vamos atualizar a página de listagem do AluraPlay no navegador e os vídeos voltarão a ser exibidos, pois agora temos acesso às variáveis.

Página do formulário

Na sequência, clicaremos no link de edição de um dos vídeos. Vamos adaptar o controller VideoFormController.php para usar o método renderTemplate() também.

A classe VideoFormController estenderá ControllerWithHtml e, em vez de require_once, chamaremos o renderTemplate(), passando o $video como contexto:

<?php
declare(strict_types=1);

namespace Alura\Mvc\Controller;

use Alura\Mvc\Entity\Video;
use Alura\Mvc\Repository\VideoRepository;

class VideoFormController extends ControllerWithHtml implements Controller
{
    public function __construct(private VideoRepository $repository)
    {
    }

    public function processaRequisicao(): void
    {
        $id = filter_input(INPUT_GET, 'id', FILTER_VALIDATE_INT);
        /** @var ?Video $video */
        $video = null;
        if ($id !== false && $id !== null) {
            $video = $this->repository->find($id);
        }

        $this->renderTemplate('video-form', [
            'video' => $video,
        ]);
    }
}

Ao atualizar o formulário no AluraPlay, a página continuará funcionando normalmente.

Para nos certificar de que essa view não precisa de nenhuma outra variável, podemos abrir o arquivo video-form.php e checar se há menções a qualquer outra variável. Vamos reparar que a única variável utilizada é $video.

Por fim, vamos atentar a um último detalhe. Atualmente, é possível instanciar uma nova classe ControllerWithHtml usando a sintaxe new ControllerWithHtml(). No entanto, essa classe não é realmente um controller — por exemplo, ela não possui o método processaRequisicao() —, então não deveria ser instanciável.

Para impedir a instanciação, vamos torná-la abstrata. Basta utilizar a palavra-chave abstract na definição da classe:

<?php

declare(strict_types=1);

namespace Alura\Mvc\Controller;

abstract class ControllerWithHtml
{
    private const TEMPLATE_PATH = __DIR__ . '/../../views/';

    protected function renderTemplate(string $templateName, array $context = []): void
    {
        extract($context);
        require_once self::TEMPLATE_PATH . $templateName . '.php';
    }
}

Além disso, definiremos que ControllerWithHtml implementa Controller:

<?php

declare(strict_types=1);

namespace Alura\Mvc\Controller;

abstract class ControllerWithHtml implements Controller
{
    private const TEMPLATE_PATH = __DIR__ . '/../../views/';

    protected function renderTemplate(string $templateName, array $context = []): void
    {
        extract($context);
        require_once self::TEMPLATE_PATH . $templateName . '.php';
    }
}

Dessa forma, todos os elementos que estenderem a classe ControllerWithHtml terão a obrigatoriedade de implementar o método processaRequisicao() da interface. Ou seja, é como se processaRequisicao() se tornasse um método abstrato em ControllerWithHtml também.

Em resumo, quando uma classe abstrata implementa uma interface mas não implementa seus métodos, é como se eles fossem métodos abstratos nessa classe. Consequentemente, todos os elementos que herdarem dessa classe abstrata precisarão implementar o método da interface.

Fizemos várias mudanças no nosso código com base em conceitos que estudamos anteriormente de orientação a objetos com PHP.

Na sequência, vamos levantar um novo questionamento: será possível retornar a string que possui o HTML do template, em vez de exibi-lo? No próximo vídeo, vamos aprender um novo conceito de PHP para responder essa questão.

Pensando em herança - Output buffer

No último vídeo, extraímos uma classe abstrata chamada ControllerWithHtmlpara representar os controllers que possuem algum template de um HTML.

Nessa classe, temos o método renderTemplate() bem como o caminho para os templates, de modo que é mais necessário repeti-lo em vários arquivos. Sendo assim, nos controllers que utilizam templates, basta chamar o método renderTemplate().

Uma das vantagens dessa abordagem é que nosso código fica mais limpo. Além disso, ela é mais próxima do que os frameworks fazem, como Laravel e Symphony.

Um detalhe comum nesses frameworks é que métodos como renderTemplate()ou render() não exibem o conteúdo diretamente. Em vez disso, eles retornam o conteúdo.

Similarmente, em nosso código, poderíamos fazer um echo do conteúdo. Por exemplo, no arquivo VideoListController.php:

// ...

    public function processaRequisicao(): void
    {
            $videoList = $this->videoRepository->all();
            echo $this->renderTemplate(
                    'video-list',
                    ['videoList' => $videoList]
            );
    }
}

Outra opção seria armazenar o conteúdo em uma variável e fazer algum processamento. Por exemplo, comprimir o HTML, remover palavras específicas ou outro tipo de manipulação do conteúdo.

Portanto, nosso próximo objetivo será alterar o método renderTemplate() para que ele retorne o conteúdo e possamos exibi-lo.

De início, vamos adicionar o echo em todos os lugares que temos a chamada do renderTemplate() — isto é, nos arquivos VideoListController.php, VideoFormController.php e LoginFormController.php. Por enquanto, o echo não exibirá nada, pois o renderTemplate() ainda não retorna nada.

Retorno de renderTemplate()

Em seguida, vamos abrir o arquivo ControllerWithHtml.php. Atualmente, na última linha do método renderTemplate(), realizamos o require_once de arquivos HTML, então sabemos que estamos exibindo algo.

Antes do require_once, vamos informar ao PHP que ele deve inicializar um buffer de saída. Ou seja, um local em que armazenaremos tudo que seria exibido na tela. Depois do require_once, vamos recuperar o conteúdo do buffer e, para poupar memória do sistema, limparemos o buffer.

Em inglês, o nome do buffer de saída é output buffer. No PHP, temos algumas funções que se iniciam com ob, de output buffer.

Para inicializar o buffer, utilizaremos o método ob_start():

<?php

declare(strict_types=1);

namespace Alura\Mvc\Controller;

abstract class ControllerWithHtml implements Controller
{
    private const TEMPLATE_PATH = __DIR__ . '/../../views/';

    protected function renderTemplate(string $templateName, array $context = []): void
    {
        extract($context);
        ob_start();
        require_once self::TEMPLATE_PATH . $templateName . '.php';
    }
}

Tudo que vier após a linha em que invocamos ob_start() e que exibiria algum conteúdo — seja um echo, um HTML ou um erro —, será armazenado no buffer, em um lugar reservado da memória.

Depois, para recuperar o conteúdo do buffer, usaremos o método ob_get_contents(). Em nosso cenário, o conteúdo é um HTML, então vamos armazená-lo em uma variável chamada $html:

<?php

declare(strict_types=1);

namespace Alura\Mvc\Controller;

abstract class ControllerWithHtml implements Controller
{
    private const TEMPLATE_PATH = __DIR__ . '/../../views/';

    protected function renderTemplate(string $templateName, array $context = []): void
    {
        extract($context);
        ob_start();
        require_once self::TEMPLATE_PATH . $templateName . '.php';
        $html = ob_get_contents();
    }
}

Para limpar o buffer, chamaremos o método ob_clean():

<?php

declare(strict_types=1);

namespace Alura\Mvc\Controller;

abstract class ControllerWithHtml implements Controller
{
    private const TEMPLATE_PATH = __DIR__ . '/../../views/';

    protected function renderTemplate(string $templateName, array $context = []): void
    {
        extract($context);
        ob_start();
        require_once self::TEMPLATE_PATH . $templateName . '.php';
        $html = ob_get_contents();
        ob_clean();
    }
}

Podemos juntar as chamadas de ob_get_contents() e ob_clean() com a função ob_get_clean()! Ela é responsável por recuperar o conteúdo e já limpar o buffer:

<?php

declare(strict_types=1);

namespace Alura\Mvc\Controller;

abstract class ControllerWithHtml implements Controller
{
    private const TEMPLATE_PATH = __DIR__ . '/../../views/';

    protected function renderTemplate(string $templateName, array $context = []): void
    {
        extract($context);
        ob_start();
        require_once self::TEMPLATE_PATH . $templateName . '.php';
        $html = ob_get_clean();
    }
}

Por fim, o retorno da função será o $html:

<?php

declare(strict_types=1);

namespace Alura\Mvc\Controller;

abstract class ControllerWithHtml implements Controller
{
    private const TEMPLATE_PATH = __DIR__ . '/../../views/';

    protected function renderTemplate(string $templateName, array $context = []): void
    {
        extract($context);
        ob_start();
        require_once self::TEMPLATE_PATH . $templateName . '.php';
        $html = ob_get_clean();
        return $html;
    }
}

Para deixar nosso código mais sucinto, em vez de declarar a variável $html e depois retorná-la, vamos simplesmente retornar o resultado de ob_get_clean():

<?php

declare(strict_types=1);

namespace Alura\Mvc\Controller;

abstract class ControllerWithHtml implements Controller
{
    private const TEMPLATE_PATH = __DIR__ . '/../../views/';

    protected function renderTemplate(string $templateName, array $context = []): void
    {
        extract($context);
        ob_start();
        require_once self::TEMPLATE_PATH . $templateName . '.php';
        return ob_get_clean();
    }
}

O phpStorm exibirá um erro no retorno, porque anteriormente definimos que o método renderTemplate() não retorna nada. Como solução, substituiremos o termo void por string na assinatura do método:

<?php

declare(strict_types=1);

namespace Alura\Mvc\Controller;

abstract class ControllerWithHtml implements Controller
{
    private const TEMPLATE_PATH = __DIR__ . '/../../views/';

    protected function renderTemplate(string $templateName, array $context = []): string
    {
        extract($context);
        ob_start();
        require_once self::TEMPLATE_PATH . $templateName . '.php';
        return ob_get_clean();
    }
}

Em resumo, estamos inicializando um buffer de saída, exibindo dados que serão armazenados no buffer e recuperando o conteúdo. Em vez de exibi-lo diretamente, nós o retornamos. Em outras palavras, o método renderTemplate() não exibe mais nada, ele apenas retorna o conteúdo.

Como teste, vamos abrir o navegador e acessar a página do formulário de edição de vídeos no AluraPlay. Em seguida, no arquivo VideoFormController.php, vamos remover o echo que inserimos há pouco. Ao atualizar a página no navegador, nada será exibido! Isso ocorre porque armazenamos o conteúdo no buffer de saída, mas não o exibimos. Inserindo o echo novamente, a página volta a aparecer. O mesmo vale para a página de listagem e para a página de login.

Dessa maneira, podemos manipular esse conteúdo. Outra opção mais comum é, em vez de exibir o conteúdo com echo, retornar algum tipo de resposta (response) com vários cabeçalhos (usando a função header()) ou com conteúdo.

Assim, em index.php, em vez de simplesmente chamar o método processaRequisicao() ao final do arquivo, pegaríamos essa resposta e faríamos um echo $response->body(), por exemplo.

Contudo, essa abordagem também tem desvantagens. Por padrão, o buffer de saída sempre existe e o próprio PHP exibe seu conteúdo automaticamente, quando julga necessário. Quando chamamos a função ob_start(), estamos sobrescrevendo esse buffer e controlando o fluxo do buffer de saída.

Sem usar o ob_start(), tínhamos a opção de usar o método flush() para exibir o buffer padrão. Por exemplo, no arquivo inicio-html.php, poderíamos inserir a função flush() após o fechamendo da tag <head>:

// CÓDIGO DE EXEMPLO

<!DOCTYPE html>
<html lang="pt-br">

<head>

  // ...
</head>

<?php flush(); ?>

// ...

Dessa forma, enviaríamos apenas o trecho de entre <!DOCTYPE html> e </head> ao navegador e continuaríamos processando o resto.

Antes, o uso do método flush() era uma opção. Agora que estamos usando um buffer de saída com ob_start(), isso não é mais possível — e essa é uma desvantagem.

Não se preocupe se você não tiver compreendido as minúcias do buffer de saída e do método flush(). Não é comum trabalhar manualmente com buffer, normalmente usa-se frameworks para lidar com essas situações. Ainda ssim, é extremamente importante saber como esses processos funcionam, por isso reservamos esse vídeo para comentar sobre ele. Você pode consultar a documentação oficial do PHP e pesquisar mais sobre o tema.

Agora, já temos nosso ControllerWithHtml lidando com o buffer de saída e retornando o conteúdo. E nossas views, caso queiram, podem exibir esse conteúdo.

Na sequência, vamos adicionar uma funcionalidade no projeto. Na interface do AluraPlay, na página de login, vamos tentar acessar o sistema com as credenciais incorretas:

Seremos redirecionados para a mesma página e, na URL, temos o parâmetro sucesso=0. Em vez de usar um parâmetro na URL, vamos exibir uma mensagem de erro para o usuário.

Nossa meta é mostrar essa mensagem sem alterar a URL, porque atualmente podemos simplesmente copiar essa URL com o parâmetro sucesso=0 e acessá-la a qualquer momento no navegador e receber uma mensagem de erro, o que não é interessante.

No próximo vídeo, aprenderemos como adicionar mensagens.

Sobre o curso PHP na Web: aplicando boas práticas e PSRs

O curso PHP na Web: aplicando boas práticas e PSRs possui 121 minutos de vídeos, em um total de 45 atividades. Gostou? Conheça nossos outros cursos de PHP em Programação, ou leia nossos artigos de Programação.

Matricule-se e comece a estudar com a gente hoje! Conheça outros tópicos abordados durante o curso:

Aprenda PHP acessando integralmente esse e outros cursos, comece hoje!

Conheça os Planos para Empresas