Boas práticas

na prática

Quem sou eu?

SOLID

  • Single Responsibility Principle
  • Open Closed Principle
  • Liskov Substitution Principle
  • Interface Segregation Principle
  • Dependency Inversion Principle

Single Responsibility Principle

uma classe só pode ter uma única razão para mudar

class Report {
    public function fetch();

    public function asHtml();

    public function asJson();
}

Como resolver?

Especialize suas classes dividindo-as até que tenham somente um objetivo

class Report {
    public function fetch();
}

class ReportWriter {
    public function asHtml(Report $report);
    public function asJson(Report $report);
}

Open Closed Principle

entidades devem ser abertas para extensão mas fechadas para modificação

É possível implementar uma nova funcionalidade em seu sistema somente adicionando novas classes ao invés de modificar as existentes?

class ReportExporter {
    public function toCsv(Report $report) {
        $tempFile = $this->createNewFile();
        $handler = fopen($tempFile, 'w');
        foreach ($report as $row) {
            fputcsv($handler, $row);
        }
        fclose($handler);
        return $tempFile;
    }

    public function toXml(Report $report) { /* ... */ }
}

Como resolver?

Crie estruturas polimórficas.

Em PHP, utilize interfaces!

class ReportExporter {
    public function export(Report $report, WriterInterface $writer) {
        $writer->init();
        foreach ($report as $row) {
            $writer->write($row);
        }
        return $writer->finish();
    }
}
interface WriterInterface {
    public function init();
    public function write($row);
    public function finish();
}
class CsvWriter implements WriterInterface { /* ... */ }
class XmlWriter implements WriterInterface { /* ... */ }

// Em meu sistema, posso trocar o Writer sem medo!
$reportExporter->export($report, new CsvWriter());
$reportExporter->export($report, new XmlWriter());

Liskov Substitution Principle

objetos em um programa deveriam ser substituíveis por instâncias de seus subtipos sem alterar a exatidão do programa

Meu sistema precisa funcionar corretamente ao trocar o tipo do Writer

$writer = new CsvWriter();
$writer->write(/* ... */);

$writer = new FileWriter();
$writer->write(/* ... */);

Pré-condições não podem ser fortalecidas em uma subclasse

class DatabaseAdapter {
    public function query(Statement $statement);
}

class MyPdoStatement extends Statement {
    /* ... */
}

class PdoDatabaseAdapter extends DatabaseAdapter {
    // Errado!
    public function query(MyPdoStatement $statement);
}

Pós-condições não podem ser enfraquecidas em uma subclasse

class Retangulo {
    public function setAltura($altura) {
        $this->altura = (float) $altura;
    }
    public function setLargura($largura) {
        $this->largura = (float) $largura;
    }
    public function getAltura() {
        return $this->altura;
    }
    public function getLargura() {
        return $this->largura;
    }
}
function area(Retangulo $retangulo) {
    return $retangulo->getAltura() * $retangulo->getLargura();
}

$retangulo = new Retangulo();
$retangulo->setAltura(5);
$retangulo->setLargura(4);

if (area($retangulo) != 20) {
    throw new UnexpectedValueException("Algo não está certo");
}
class Quadrado extends Retangulo {
    public function setAltura($altura) {
        $this->altura = (float) $altura;
        $this->largura = (float) $altura;
    }
    public function setLargura($largura) {
        $this->altura = (float) $largura;
        $this->largura = (float) $largura;
    }
}

O que acontece ao executar o mesmo código do cálculo de área, mas passando um objeto Quadrado?

function area(Retangulo $retangulo) {
    return $retangulo->getAltura() * $retangulo->getLargura();
}

$quadrado = new Quadrado();
$quadrado->setAltura(5);
$quadrado->setLargura(4);

if (area($quadrado) != 20) {
    throw new UnexpectedValueException("Algo não está certo");
}

Como resolver?

Herança não deve ser utilizada quando uma subclasse restringe a liberdade imposta na classe principal, mas sim quando adiciona mais detalhes.

Ela só é necessária quando há similaridade de comportamento. Senão, use composição.

Interface Segregation Principle

é preferível ter várias interfaces mais específicas do que uma genérica

interface AveInterface {
    public function bicar();
    public function voar();
}

Mas nem toda ave voa!

class Pinguim implements AveInterface {
    public function bicar() { /* ... */ }
    public function voar() {
        return false;
    }
}

Como resolver?

Segrege suas interfaces ao invés de criar uma que atenda todos os casos

interface AveInterface {
    public function bicar();
}

interface VoadorInterface {
    public function voar();
}

class Papagaio implements AveInterface, VoadorInterface { /* ... */ }
class Pinguim implements AveInterface { /* ... */ }
class Esquilo implements VoadorInterface { /* ... */ }

Dependency Inversion Principle

dependa de abstrações (interfaces) ao invés de classes concretas

PSR-15
php-fig.org/psr/psr-15

use Psr\Http\Server\RequestHandlerInterface;
use Psr\Http\Message\ServerRequestInterface;
use Psr\Http\Message\ResponseInterface;
use Zend\Diactoros\Response\JsonResponse;

class ActionHandler implements RequestHandlerInterface {
    public function handle(ServerRequestInterface $request) : ResponseInterface {
        return new JsonResponse(['status' => true]);
    }
}

Depender de interfaces nos fornece grande flexibilidade para mudar partes do sistema sem afetar o funcionamento do todo

Design by contract

Object Calisthenics

  • Only One Level Of Indentation Per Method
  • Don't Use The ELSE Keyword
  • Wrap All Primitives And Strings
  • First Class Collections
  • One Dot Per Line
  • Don't Abbreviate
  • Keep All Entities Small
  • No Classes With More Than Two Instance Variables
  • No Getters/Setters/Properties

Only One Level Of Indentation Per Method

public function minify(array $files) {
    foreach ($files as $file) {
        if (!is_file($file)) {
            throw new RuntimeException("{$file} não é válido");
        }
        switch (pathinfo($file, PATHINFO_EXTENSION)) {
            case 'js':
                // ...
                break;

            case 'html':
                // ...
                break;

            case 'css':
                // ...
                break;
        }
    }
}
public function minify(array $files) {
    foreach ($files as $file) {
        $this->parseFile($file);
    }
}

protected function parseFile($file) {
    if (!is_file($file)) {
        throw new RuntimeException("{$file} não é válido");
    }
    $this->getParserForExtension(pathinfo($file, PATHINFO_EXTENSION))
        ->parse($file);
}

protected function getParserForExtension($extension) {
    if (isset($this->parsersByExtension[$extension])) {
        return $this->parsersByExtension[$extension];
    }

    throw new DomainException("Nenhum minificador para {$extension} foi encontrado");
}

Don't Use The ELSE Keyword

public function login($username, $password) {
    $row = $this->dbh->fetch('SELECT * FROM user WHERE username = ?', $username);
    if (!empty($row)) {
        if ($row['active']) {
            if (password_verify($password, $row['password'])) {
                $_SESSION['loggedIn'] = true;
                header('Location: /dashboard.php');
                return true;
            } else {
                header('Location: /login.php?error=invalid');
            }
        } else {
            header('Location: /login.php?error=inactive');
        }
    } else {
        $attempts = $this->dbh->fetch('SELECT attempts FROM login_attempts WHERE username = ?', $username);
        if ($attempts >= self::MAX_LOGIN_ATTEMTPS) {
            $this->dbh->query('UPDATE login_attempts SET attempts = attempts + 1 WHERE username = ?', $username);
            header('Location: /login.php?error=blocked');
        } else {
            header('Location: /login.php?error=invalid');
        }
    }
    return false;
}

Early return

public function login($username, $password) {
    $user = $this->repository->findByUsername($username);
    if (empty($user)) {
        $this->checkLoginAttempts($user);
        return false;
    }

    return $this->checkUser($user);
}

protected function checkUser(User $user) {
    if (!$user->active) {
        header('Location: /login.php?error=inactive');
        return false;
    }

    if (!password_verify($password, $user->password)) {
        header('Location: /login.php?error=invalid');
        return false;
    }

    $this->doLogin($user);
    retun true;
}

protected function doLogin(User $user) {
    $_SESSION['loggedIn'] = true;
    header('Location: /dashboard.php');
}

protected function checkLoginAttempts(User $user) {
    $attempts = $this->repository->touchLoginAttemptsByUser($user);
    if ($attempts >= self::MAX_LOGIN_ATTEMTPS) {
        header('Location: /login.php?error=blocked');
        return;
    }

    header('Location: /login.php?error=invalid');
}

Wrap All Primitives And Strings

public function parse($email) {
    if (!filter_var($email, FILTER_VALIDATE_EMAIL)) {
        throw new InvalidArgumentException('Email inválido');
    }
    if (!EmailValidator::checkExistingDomain($email)) {
        throw new InvalidArgumentException('Email inválido');
    }
    // ...
}

Se houver comportamento, crie um objeto!

class Email {
    public function __construct($email) {
        if (!filter_var($email, FILTER_VALIDATE_EMAIL)) {
            throw new InvalidArgumentException('Email inválido');
        }
        // outras validações
    }
}
class Example {
    public function parse(Email $email) {
        // ...
    }
}

First Class Collections

Se a classe possui uma coleção, ela não deve ter outras propriedades

class FilterApplier {
    protected $data = [];
    protected $index;
    public function add(callable $filter);
    public function remove(callable $filter);
    public function run();
}

Crie collections!

class FilterApplier {
    public function __construct(FilterCollection $filterCollection);
    public function run();
}
class FilterCollection implements Iterator {
    protected $data = [];
    public function add(callable $filter);
    public function remove(callable $filter);
}

One Dot Per Line

Se você precisa passar por diversos objetos para chamar um método, significa que você precisa saber muito sobre como eles funcionam

class Controller {
    public function run() {
        if ($this->getRequest()->getHeaders()->get('Accept') == 'application/json') {
            header('Content-type: application/json');
            return json_encode(['status' => true]);
        }
    }
}

Law of Demeter e Tell, Don't Ask

class Controller {
    public function run() {
        if ($this->getRequest()->needsJson()) {
            header('Content-type: application/json');
            return json_encode(['status' => true]);
        }
    }
}
class Request {
    public function needsJson() {
        return $this->getHeaders()->needsJson();
    }
}
class Headers {
    public function needsJson() {
        return $this->get('Accept') === 'application/json';
    }
}

No Getters/Setters/Properties

class Login {
    public function login(User $user) {
        // ...
        $attempts = $this->loginAttempts->getByUser($user);
        $this->loginAttempts->setByUser($user, ++$attempts);
        if ($attempts >= self::MAX_LOGIN_ATTEMTPS) {
            $this->loginAttempts->blockUserLogin($user);
        }
    }
}

“Tell, Don't Ask”

class Login {
    public function login(User $user) {
        // ...
        $this->loginAttempts->touchUser($user);
    }
}

PHPCS

github.com/squizlabs/PHP_CodeSniffer

Plugin de Object Calisthenics para o PHPCS

Verifica violações do código de acordo com um conjunto de regras

Screenshot de uma execução do PHPCS mostrando erros e alertas no código

PHPCBF

Automaticamente corrige algumas violações no código

Screenshot de uma execução do PHPCBF corrigindo violações no código

PHPMD

phpmd.org

Equivalente à ferramenta PMD do Java, analisa código para detectar diversas anomalias, como:

  • Nomes muito pequenos ou grandes;
  • Códigos muito longos ou complexos;
  • Parâmetros e variáveis inutilizados;
  • e muitos outros

Cyclomatic Complexity

Complexidade determinada de acordo com o número de pontos de decisão (if, while, for e case) + 1

public function minify(array $files) {                         // 1
    foreach ($files as $file) {                                 // 2
        if (!is_file($file)) {                                  // 3
            throw new RuntimeException("{$file} não é válido");
        }
        switch (pathinfo($file, PATHINFO_EXTENSION)) {
            case 'js':                                          // 4
                // ...
                break;

            case 'html':                                        // 5
                // ...
                break;

            case 'css':                                         // 6
                // ...
                break;
        }
    }
}

Graus de Complexidade

  •  1-4 — baixa
  •  5-7 — moderada
  • 8-10 — alta
  •  11+ — muito alta

Mais tópicos

Referências

Obrigado!

vcampitelli.github.io