Skip to content

Instantly share code, notes, and snippets.

@netojoaobatista
Created December 4, 2015 22:22
Show Gist options
  • Star 6 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save netojoaobatista/ddc63adb72971eb6164b to your computer and use it in GitHub Desktop.
Save netojoaobatista/ddc63adb72971eb6164b to your computer and use it in GitHub Desktop.
Apenas algumas novidades do PHP 7.

O impacto do PHP 7 para a orientação a objetos

Se você desenvolve em PHP utilizando orientação a objetos, então você certamente está, assim como eu, excitado com o lançamento do PHP 7. O PHP 7 é, sem dúvidas, um novo divisor de águas na história da linguagem; assim como o foi a versão 5.3, diversos novos recursos importantes estão sendo adicionados e, assim como os namespaces mudaram a forma de programar orientado a objetos a partir do PHP 5.3, os novos recursos do PHP 7 mudarão, novamente, a forma de programar orientado a objetos no PHP.

O que é orientação a objetos?

Para compreender efetivamente qual será o impacto do PHP 7 para a orientação a objetos, precisamos primeiro definir o que é orientação a objetos. Você certamente já ouviu falar, ou leu em algum lugar, que orientação a objetos é a tentativa de representar objetos do mundo real na forma de partículas que compõem o software. Pessoalmente, acho essa definição muito ruim; de fato, acho que essa definição mais dificulta a compreenção a cerca do que é orientação a objetos, do que efetivamente define alguma coisa.

De forma bem simples e direta, orientação a objetos trata-se de contratos e confiança. Estabelecemos uma série de parâmetros e limites para a forma como alguma coisa deve se comportar - esses parâmetros definem o contrato. Um participante da aplicação assina esse contrato e confiamos que esse contrato será cumprido. Como a coisa será efetivamente feita é, do ponto de vista da orientação a objetos, irrelevante. Tudo o que precisamos, é que o participante que assinou o contrato, cumpra com as regras do contrato.

As limitações do PHP 5.x

O grande problema do PHP 5.x na definição de contratos, está na incapacidade de estabelecermos limites, ou seja, precisamos confiar no desenvolvedor que implementa uma interface, em vez de confiar na interface; não há, no PHP 5.x, uma forma de garantir, através do design de uma interface, limites para entrada e saída.

Por exemplo, se tivermos um método something que espera receber uma string, inteiro, um boolean, etc., não temos como garantir isso na interface. Nossa única saída é testar se quem está consumindo esse método, o está fazendo da forma correta. Da mesma forma, não há como garantir que o retorno do método será aquilo que é esperado por quem o está consumindo; então, se quisermos garantir a integridade da aplicação, precisamos verificar se o retorno é aquilo que estamos esperando.

Esse tipo de programação defensiva, com verificações no input e no output, além de diminuir a confiança pretendida pela orientação a objetos, adicionam ruídos no código. Esses ruídos podem, além de dificultar a compreensão de algorítimos, aumentar o custo de manutenção.

Tipagem de retorno e tipos primitivos

A primeira grande adição para a orientação a objetos do PHP 7 é, justamente, uma ferramenta que proporcionará uma maior confiança para nossas interfaces. Para ilustrar essa confiança, veja o contraste dos dois fragmentos abaixo:

Versão PHP 5.x

Definição do método

public function sum($left, $right)
{
    if (is_numeric($left) && is_numeric($right)) {
	    return $left + $right;
	}

	throw new \InvalidArgumentException('...');
}

Utilização do método

try {
    $result = $instance->sum($a, $b);

	if (!is_numeric($result)) {
	    // fallback
	}

	//...
} catch (\InvalidArgumentException $e) {
    //...
}

Versão PHP 7

Definição do método

public function sum(float $left, float $right): float
{
    return $left + $right;
}

Utilização do método

$result = $instance->sum($a, $b);

É notório, ao observar os fragmentos de código acima, que a versão em PHP 7 é muito mais fluída. O fato de não existir ruídos de programação defensiva, facilita a leitura e compreensão do código; quanto mais fácil é a leitura e compreensão do código, mais fácil é a manutenção.

Derivações e implementações

As regras para definição de tipo de retorno seguem as mesmas regras da definição de tipo dos parâmetros, ou seja, se estivermos derivando uma classe que define um tipo, a derivação precisa, necessariamente, ser compatível com a definição da interface ou da classe base.

interface Otherthing
{
}

interface Something
{
    public function doSomething(): Otherthing;
}

Se formos implementar a interface Something, precisaremos definir o método doSomething exatamente como especificado na interface. Por exemplo:

class Sample implements Something
{
    public function doSomething(): Otherthing
    {
        // ...
    }
}

Se nosso método doSomething retornar qualquer coisa que não implemente Otherthing, um TypeError será disparado. Claro que nada impede, porém, que derivemos uma interface para retornar uma instância da interface derivada. Por exemplo:

interface Otherthing
{
}

interface Something
{
    public function doSomething(): Otherthing;
}

interface SomeOtherthing extends Otherthing
{
}

class Sample implements Something
{
    public function doSomething(): Otherthing
    {
        return new class implements SomeOtherthing {};
    }
}

O problema dessa abordagem é que, por não saber que o método Sample::doSomething retorna uma instância de SomeOtherthing, confiaremos na interface e esperaremos apenas um Otherthing.

Novo comportamento para erros e nova hierarquia de exceções

Se tem uma coisa, ao meu ver, que era realmente chata no PHP 5.x, era a forma como o PHP tratava erros. Agora o PHP 7 permite que erros fatais sejam tratados como exceção; isso significa que é possível prever e criar alternativas para possíveis erros. Por exemplo, se estivermos trabalhando com uma bilioteca que não foi desenvolvida com tipagem de retorno e, em vez de retornar o objeto que a interface promete, entregar um null. Se tentarmos acessar um método em null no PHP 5.x, receberemos um Fatal Error; apesar do PHP 7 também gerar um Fatal Error, conseguimos "pegar" esse erro como se fosse uma exceção!

Versão PHP 5.x

public function createSomething()
{
    // ...

	return null;
}

public function doSomething()
{
    $this->createSomething()->execute();
}
$instance->doSomething(); //Fatal Error

Versão PHP 7

public function createSomething()
{
    // ...

	return null;
}

public function doSomething()
{
    $this->createSomething()->execute();
}
try {
    $instance->doSomething();
} catch (\Error $e) {
    // fallback
}

Podemos, inclusive, pegar erros de tipo. Se estivermos utilizando uma biblioteca X qualquer, onde um participante promete retornar uma instância de qualquer coisa mas retorna uma outra coisa qualquer - um false ou null. Conseguimos pegar essa violação de contrato na hora da injeção da dependência:

try {
    $dependency = new SomeDependencyFactory();

    $instance = new Something();
	$instance->doSomething($dependency->createOtherthing());
} catch (\TypeError $e) {
    // ...
}

Isso permite duas coisas: primeiro que nosso código permaneça limpo, já que não precisaremos fazer verificações internas do tipo if (is_null($something)); e, segundo, que temos a possibilidade de um caminho alternativo durante a tentativa da injeção da dependência. Tratamos os erros nos momentos e lugares onde devem ser tratados, evitando ruídos desnecessários em outros lugares.

Compatibilidade

Antes do PHP 7, todas as exceções derivavam da classe \Exception. Para garantir a compatibilidade com códigos escritos em versões anteriores ao PHP 7, uma nova interface Throwable foi criada. Isso foi feito para evitar que blocos como o abaixo pegassem erros de parser, de tipagem e outros, quando isso não era esperado que ocorresse:

try {
    // ...
} catch (\Exception $e) {
    // ...
}

Sem a nova interface Throwable, um erro de divisão por zero, por exemplo, poderia ser pego num bloco try/catch sem que isso fosse esperado, causando diversos problemas. A nova interface ficou assim:

interface Throwable
    |- Exception implements Throwable
        |- ...
    |- Error implements Throwable
        |- TypeError extends Error
        |- ParseError extends Error
        |- AssertionError extends Error
        |- ArithmeticError extends Error
            |- DivisionByZeroError extends ArithmeticError

A hierarquia da \Exception é a mesma, exceto que agora ela implementa a interface Throwable; além da \Exception, agora temos também \Error e toda uma nova hierarquia.

Expectativas e código auto-testável

Apesar do PHP 7 vir com novos recursos para definição de entrada e saída na interface - inclusive com tipos primitivos -, ainda existe uma situação em que não conseguimos resolver: se uma interface precisa entregar um Iterator de instâncias de Something, não conseguimos garantir que todos os elementos do Iterator sejam, de fato, instâncias Something. Se simplesmente confiarmos que todas as instâncias são de Something e, por qualquer motivo, apenas uma não for, nosso código irá falhar. Como não há um meio para garantir os tipos dos elementos do Iterator, precisamos testar elemento por elemento durante a iteração:

class Otherthing implements IteratorAggregate
{
    public function getIterator()
    {
        yield new Something();
        yield 3;
        yield new Something();
        yield new Something();
        yield new Something();
        yield new Something();
        yield new Something();
    }
}

class Client
{
    public function foreach(Otherthing $otherthing)
    {
        foreach ($otherthing as $something) {
             if (!$something instanceof Something) {
                throw new \UnexpectedValueException(
				    'All elements of Iterator must be instances of Something.'
				);
            }

            // ...
        }
    }
}

Existem diversos motivos para esse tipo de situação ocorrer. Um dos motivos mais comuns, entretanto, é falha de integração. Por maior que seja a cobertura dos nossos testes unitários e por mais cenários, inclusive negativos, que testemos, se não testarmos a integração entre os participantes da aplicação, erros como o ilustrado acima podem ocorrer.

O PHP 5.x já possui uma função chamada assert, mas o PHP 7 mudou completamente seu comportamento. Agora podemos fazer esses testes durante a execução, disparar exceções específicas para frustrações e, ainda, definir para que o custo desses testes seja zero em produção. Isso é feito da seguinte forma:

  • assert.exception
    • Se assert.exception for igual a 0, então nenhuma exceção será disparada.
    • Se assert.exception for igual a 1, então exceções serão disparadas e poderemos pegá-las com um bloco catch.
  • zend.assertions
    • Se zend.assertions for igual a 1, então os códigos dos testes serão gerados e executados - isso deve ser utilizado em ambiente de desenvolvimento.
    • Se zend.assertions for igual a 0, então os códigos dos testes serão gerados, mas ignorados durante a execução.
    • Se zend.assertions for igual a -1, então os códigos dos testes não serão gerados, promovendo custo zero para ambiente de produção.
class Client
{
    public function foreach(Otherthing $otherthing)
    {
        foreach ($otherthing as $something) {
		    assert(
                $something instanceof Something,
                'All elements of Iterator must be instances of Something.'
            );

            // ...
        }
    }
}

Claro que a possibilidade de definir as expectativas e essa definição ter custo zero em produção não significa que podemos deixar de programar defensivamente; devemos continuar testando todos os elementos do Iterator, mas esse "não tão novo" recurso permite que tenhamos um código auto-testável e que os testes sirvam de documentação do algorítimo.

Classes anônimas

Classes anônimas são como classes normais, exceto pelo fato de não terem nome e serem criadas em tempo de execução, através de uma expressão que resulta numa instância dessa classe. Pode ser um dos recursos mais poderosos do PHP, principalmente quando aplicada em casos de uso adequados; mas também podem vir a ser um pesadelo durante a manutenção do software, se utilizadas sem qualquer cuidado com suas consequências.

A criação de uma classe anônima é bastante simples; vamos supor que tenhamos uma interface Something que será injetada no participante Otherthing. Se a interface Something for simples e a injeção for ocorrer de forma localizada, ou seja, num único ponto da aplicação, e não voltar a ocorrer em outros pontos, então uma classe anônima pode ser utilizada:

interface Something
{
    public function doSomething();
}


class Otherthing
{
    public function execute(Something $something)
	{
	    return $something->doSomething();
	}
}
$otherthing = new Otherthing();
$otherthing->execute(new class implements Something
{
    public function doSomething()
	{
	    //...
	}
});

Assim como as classes normais, classes anônimas também podem ser derivações de outras classes e/ou utilizar traits em sua composição:

abstract class AbstractSomething
{
    protected $result = [];

    // template method
	public function doSomething()
	{
	    //...

	    while ($condition) {
	        $this->result[] = $this->process($someData);
			// ...
		}
	}

	abstract public function process($someData);
}

trait SomeTrait
{
    public function process($someData)
	{
	    // ...

		return $someResult;
	}
}
$otherthing = new Otherthing();
$otherthing->execute(new class extends AbstractSomething implements IteratorAggregate
{
    use SomeTrait;

	public function getIterator()
	{
	    yield from $this->result;
	}
});

Outro cenário de uso, é na criação de Adapters para interfaces incompatíveis, por exemplo:

interface ExpectedInterface
{
    public function execute();
}

class Something
{
    public function doSomething(ExpectedInterface $expectedInterface)
	{
	    // ...

		$expectedInterface->execute();
	}
}

A classe Something precisa da ExpectedInterface para fazer alguma coisa. Se estivermos utilizando um framework qualquer, ou tivermos um código legado, e tivermos que utilizar um participante que faça o mesmo que a ExpectedInterface, mas que possua uma interface incompatível, precisaremos, no PHP 5.x, criar uma classe física para fazer a adaptação das interfaces. Com o PHP 7, a adaptação das interfaces pode ser feita em tempo de execução:

class Incompatible
{
    public function run()
	{
	    // ...
	}
}
$something = new Something();
$something->doSomething(new class extends Incompatible implements ExpectedInterface
{
    public function execute()
    {
        $incompatible = new Incompatible();
		$incompatible->run();
    }
});

É claro que, como Adapter, essa abordagem deve ser utilizada apenas se a interface incompatível for utilizada apenas em um ponto da aplicação. Se houver mais do que um ponto da aplicação onde a interface incompatível é necessária, então a classe física passa a ser uma abordagem melhor.

Como podemos implementar métodos de classes abstratas, interfaces e utilizar traitas na composição da classe anônima, esse recurso passa a ser uma excelente ferramenta para mocks em testes unitários; podemos, em vez de utilizar uma API complexa para criação de um mock de uma classe abstrata, simplesmente injetar a classe anônima na unidade que está sendo testada, simplificando muito a implementação e a leitura do código dos testes.

Delegação de generators

Os generators foram implementados no PHP 5.5; é um recurso bacana que permite a criação de um iterator sem que seja necessário implementar a interface Iterator. Mas se eu precisar que meu iterator tenha 10 elementos, eu precisarei de uma estrutura como um foreach, for, while, etc, fazendo um yield para cada elemento. Basicmente, eu preciso de um foreach dentro do método para gerar o Iterator e um foreach fora do método, para iterar os elementos:

Versão PHP >= 5.5

public function getIterator()
{
    for ($i = 0; $i < 10; ++$i) {
	    yield $i;
    }
}

Com a delegação de generators, o PHP 7 facilida ainda mais a utilização dos generators.

Versão PHP 7

public function getIterator()
{
    yield from range(0,10);
}

Com o yield from conseguimos delegar o generator para qualquer coisa Traversable, como um array, um outro Iterator, outros métodos - mesmo métodos de classes injetadas -, etc.

Retorno em generators

Outro recurso bacana relacionado com os generators, é a possibilidade de retornar um valor ao fim do Iterator. Por exemplo, imagine que tenhamos que iterar sobre os bytes de um arquivo. Como no PHP >= 5.5 não havia a possibilidade de retornar valores em generators, precisávamos de um novo método apenas para obter um valor relacionado com o Iterator, poluindo, por consequência, nossa interface:

PHP >= 5.5

public function getBytes($length)
{
    while (!feof($this->fh)) {
        yield fread($this->fh, $length);
    }
}

public function getPosition()
{
    return ftell($this->fh);
}

PHP 7

 public function getBytes(int $length): Iterator
{
    while (!feof($this->fh)) {
        yield fread($this->fh, $length);
    }

    return ftell($this->fh);
}

Com isso, é possível, no mesmo método, iterar os bytes do arquivo e, ainda, retornar quantos bytes foram iterados:

$file = new File('filename');
$bytes = $file->getBytes();

foreach ($bytes as $byte) {
    // ...
}

printf("Foram lidos %d bytes do arquivo\n", $bytes->getReturn());
@googleinurl
Copy link

Isso está lindo cara!

@gvsrepins
Copy link

Excelente! Obrigado por compartilhar @netojoaobatista.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment