Versão mais recente: 1.0.0-beta
Biblioteca PHP 5.4+ para representar relações de equivalência e estratégias para hash e classificação de valores.
As relações de equivalência são úteis para estabelecer uma forma generalizada de comparação de valores em relação a requisitos específicos de domínio, bem como para representar critérios personalizados para comparação de valores em contextos limitados, especialmente para uso em coleções.
Recursos complementares, como hashing e classificação, também são cobertos por esta biblioteca, tornando-a uma adição valiosa ao seu conjunto de ferramentas de desenvolvimento.
A API está amplamente documentada no código-fonte. Além disso, uma versão HTML também está disponível para visualização mais conveniente no navegador.
Use o Composer para instalar o pacote:
$ composer require phpcommon/comparison
Uma relação é uma ferramenta matemática para descrever associações entre elementos de conjuntos. As relações são amplamente utilizadas na ciência da computação, especialmente em bancos de dados e aplicações de agendamento.
Ao contrário da maioria das linguagens modernas, o PHP não suporta sobrecarga de operadores, historicamente evitada como opção de design. Em outras palavras, não é possível substituir o comportamento padrão dos operadores nativos, como igual, idêntico, maior que, menor que, etc. Por exemplo, Java fornece a interface Comparable, enquanto Python fornece alguns métodos mágicos.
A importância de tal conceito fica mais evidente em situações em que a noção de equivalência ou ordenação varia de acordo com o objeto de comparação ou com o contexto, conforme discutido nas seções seguintes.
Em matemática, uma relação de equivalência é uma relação binária reflexiva , simétrica e transitiva . No campo da computação, porém, há outra propriedade que deve ser levada em consideração: a consistência . Consistência significa que uma relação não deve produzir resultados diferentes para a mesma entrada.
Uma relação de equivalência onipresente é a relação de igualdade entre elementos de qualquer conjunto. Outros exemplos incluem:
Para os fins desta biblioteca, uma relação de equivalência pode ser genérica ou específica de tipo. As relações específicas de tipo são definidas pela implementação de interfaces Equatable
ou Equivalence
, enquanto as equivalências genéricas devem implementar a última.
A interface Equatable
define um método generalizado que uma classe implementa para criar um método específico de tipo para determinar a igualdade de instâncias.
Para ilustrar, considera uma classe Money
, que visa representar valores monetários. Esta classe é uma boa candidata para implementar a interface Equatable
, pois Money
é um Objeto de Valor, ou seja, a noção de igualdade desses objetos não é baseada em identidade. Em vez disso, duas instâncias de Money
serão iguais se tiverem os mesmos valores. Assim, enquanto Money::USD(5) === Money::USD(5)
retorna false
, Money::USD(5)->equals(Money::USD(5))
retorna true
.
Aqui está a classe mencionada anteriormente:
final class Money implements Equatable
{
private $ amount ;
private $ currency ;
public function __construct ( $ amount , $ currency )
{
$ this -> amount = ( int ) $ amount ;
$ this -> currency = ( string ) $ currency ;
}
public function equals ( Equatable $ other )
{
if (! $ other instanceof self) {
return false ;
}
return $ this -> amount === $ other -> amount && $ this -> currency === $ other -> currency ;
}
}
Há muitos casos, porém, em que se torna necessário ter uma maneira não padronizada ou externa de comparar dois valores. Talvez o caso de uso mais óbvio para essas relações personalizadas seja para uso com coleções, mas também é útil para fornecer esses recursos para valores escalares ou para uma classe existente que não pode fornecê-los por si só, porque pertence a um pacote de terceiros ou foi construído em PHP.
Suponha que você esteja desenvolvendo um software para ajudar hospitais a gerenciar doações de sangue. Uma das exigências diz que o enfermeiro não pode coletar sangue de doadores que tenham o mesmo tipo sanguíneo. Uma relação para este cenário seria assim:
use PhpCommon Comparison Equivalence ;
class BloodGroupEquivalence implements Equivalence
{
public function equals ( Equatable $ other )
{
return get_class ( $ other ) === static ::class;
}
public function equivalent ( $ left , $ right )
{
if (! $ left instanceof Person) {
UnexpectedTypeException:: forType (Person::class, $ left );
}
if (! $ right instanceof Person) {
return false ;
}
return $ left -> getBloodType () === $ right -> getBloodType ();
}
}
Esta relação determina se duas pessoas são do mesmo grupo sanguíneo:
$ equivalence = new BloodGroupEquivalence ();
$ donors = new BloodDonors ( $ equivalence );
$ james = new Person ( ' James ' , ' A ' );
$ john = new Person ( ' John ' , ' A ' );
// James and John are considered equivalent once they are of the same blood group
var_dump ( $ equivalence -> equivalent ( $ james , $ john )); // Outputs bool(true)
// Initially, none of them are present in the collection
var_dump ( $ volunteers -> contains ( $ james )); // Outputs bool(false)
var_dump ( $ volunteers -> contains ( $ john )); // Outputs bool(false)
// Add James to the set of volunteers
$ donors -> add ( $ james );
// Now, considering only the blood group of each donor for equality, both of
// them are considered present in the collection
$ donors -> contains ( $ james ); // Outputs bool(true)
$ donors -> contains ( $ john ); // Outputs bool(true)
Como BloodGroupEquivalence
estabelece uma relação de equivalência entre pessoas com base no seu grupo sanguíneo, qualquer tentativa de adicionar John à coleção será ignorada, pois James já está presente e eles são do mesmo tipo sanguíneo.
A princípio pode parecer um pouco complicado um requisito simples, mas em casos reais pode ser usado para comparar a equivalência entre tipos sanguíneos compatíveis, a fim de dividir os doadores em grupos.
Esta biblioteca fornece algumas relações de equivalência genéricas como parte da biblioteca padrão, conforme descrito abaixo.
Compara dois valores para identidade.
Esta relação é baseada no operador idêntico. Na maioria dos casos, dois valores são considerados equivalentes se tiverem o mesmo tipo e valor, mas há algumas exceções:
NAN
é desigual a qualquer outro valor, inclusive a si mesmo.A tabela a seguir resume como os operandos dos vários tipos são comparados:
$A $B | NULO | Booleano | Inteiro | Flutuador | Corda | Recurso | Variedade | Objeto |
---|---|---|---|---|---|---|---|---|
NULO | true | false | false | false | false | false | false | false |
Booleano | false | $A === $B | false | false | false | false | false | false |
Inteiro | false | false | $A === $B | false | false | false | false | false |
Flutuador | false | false | false | $A === $B | false | false | false | false |
Corda | false | false | false | false | $A === $B | false | false | false |
Recurso | false | false | false | false | false | $A === $B | false | false |
Variedade | false | false | false | false | false | false | $A === $B | false |
Objeto | false | false | false | false | false | false | false | $A === $B |
Compara dois valores para igualdade.
A equivalência de valor se comporta exatamente como a equivalência de identidade, exceto que delega a comparação entre objetos Equatable
aos objetos que estão sendo comparados. Além disso, relações externas podem ser especificadas para comparar valores de um tipo específico. É útil nos casos em que é desejável substituir o comportamento padrão para um tipo específico, mas manter todos os outros. Também é útil para definir uma relação para objetos de classes que pertencem a pacotes de terceiros ou incorporados ao PHP.
As seguintes regras são usadas para determinar se dois valores são considerados equivalentes:
NAN
é desigual a qualquer outro valor, inclusive a si mesmo.Equatable
e a expressão $left->equals($right)
é avaliada como true
.$relation->equivalent($left, $right)
é avaliada como true
.A tabela a seguir resume como os operandos dos vários tipos são comparados:
$A $B | NULO | Booleano | Inteiro | Flutuador | Corda | Recurso | Variedade | Objeto | Equatável |
---|---|---|---|---|---|---|---|---|---|
NULO | true | false | false | false | false | false | false | false | false |
Booleano | false | $A === $B | false | false | false | false | false | false | false |
Inteiro | false | false | $A === $B | false | false | false | false | false | false |
Flutuador | false | false | false | $A === $B | false | false | false | false | false |
Corda | false | false | false | false | $A === $B | false | false | false | false |
Recurso | false | false | false | false | false | $A === $B | false | false | false |
Variedade | false | false | false | false | false | false | eq($A, $B) | false | false |
Objeto | false | false | false | false | false | false | false | $A === $B | false |
Equatável | false | false | false | false | false | false | false | false | $A‑>equals($B) |
Onde eq()
denota uma função que compara cada par de entradas correspondentes recursivamente, de acordo com as regras descritas acima.
Esta relação também fornece uma maneira de substituir a lógica de equivalência para uma classe específica sem a necessidade de criar uma nova relação. Por exemplo, suponha que você queira comparar instâncias de DateTime
com base em seus valores, mas manter o comportamento padrão para os outros tipos. Isso pode ser feito especificando uma relação personalizada a ser usada sempre que uma instância de DateTime
for comparada com outro valor:
use PhpCommon Comparison Hasher ValueHasher as ValueEquivalence ;
use PhpCommon Comparison Hasher DateTimeHasher as DateTimeEquivalence ;
use DateTime ;
$ relation = new ValueEquivalence ([
DateTime::class => new DateTimeEquivalence ()
]);
$ date = ' 2017-01-01 ' ;
$ timezone = new DateTimeZone ( ' Pacific/Nauru ' );
$ left = new DateTime ( $ date , $ timezone );
$ right = new DateTime ( $ date , $ timezone );
// Outputs bool(true)
var_dump ( $ relation -> equivalent ( $ left , $ right ));
Compara dois valores para igualdade semântica.
Uma equivalência semântica está planejada para versões futuras. Isso permitiria a comparação de valores que parecem semanticamente semelhantes - mesmo que sejam de tipos diferentes. É semelhante ao modo como a comparação flexível funciona no PHP, mas sob condições mais restritivas, de tal forma que as propriedades de reflexividade, simetria e transitividade se mantêm.
Compara duas instâncias DateTime
com base em data, hora e fuso horário.
Esta relação considera duas instâncias de DateTime
equivalentes se tiverem a mesma data, hora e fuso horário:
use PhpCommon Comparison Hasher IdentityHasher as IdentityEquivalence ;
use PhpCommon Comparison Hasher DateTimeHasher as DateTimeEquivalence ;
use DateTime ;
$ identity = new IdentityEquivalence ();
$ value = new DateTimeEquivalence ();
$ date = ' 2017-01-01 ' ;
$ timezone = new DateTimeZone ( ' Pacific/Nauru ' );
$ left = new DateTime ( $ date , $ timezone );
$ right = new DateTime ( $ date , $ timezone );
// Outputs bool(false)
var_dump ( $ identity -> equivalent ( $ left , $ right ));
// Outputs bool(true)
var_dump ( $ value -> equivalent ( $ left , $ right ));
Em PHP, as chaves do array só podem ser representadas como números e strings. No entanto, há vários casos em que é útil armazenar tipos complexos como chaves. Tomemos como exemplo classes que representam diferentes tipos de números ou strings, como objetos GMP, strings Unicode, etc. Seria conveniente poder usar esses objetos também como chaves de array.
Para preencher esta lacuna, esta biblioteca apresenta as interfaces Hashable
e Hasher
, que especificam um protocolo para fornecer códigos hash para valores. Essas interfaces não exigem que os implementadores forneçam funções de hash perfeitas. Ou seja, dois valores que não são equivalentes podem ter o mesmo código hash. Porém, para determinar se dois valores com o mesmo código hash são, de fato, iguais, os conceitos de hashing e equivalência devem ser combinados de forma complementar. Explica porque Hasher
e Hashable
estendem Equivalence
e Equatable
respectivamente.
Uma palavra de advertência
Um código hash destina-se à inserção e pesquisa eficientes em coleções baseadas em uma tabela hash e para verificações rápidas de desigualdade. Um código hash não é um valor permanente. Por este motivo:
- Não serialize valores de código hash nem os armazene em bancos de dados.
- Não use o código hash como chave para recuperar um objeto de uma coleção com chave.
- Não envie códigos hash entre domínios ou processos de aplicativos. Em alguns casos, os códigos hash podem ser calculados por processo ou por domínio de aplicativo.
- Não use o código hash em vez de um valor retornado por uma função de hash criptográfico se precisar de um hash criptograficamente forte.
- Não teste a igualdade de códigos hash para determinar se dois objetos são iguais, uma vez que valores desiguais podem ter códigos hash idênticos.
Há casos em que pode ser desejável definir uma lógica de hashing personalizada para uma classe que melhor atenda aos seus requisitos. Por exemplo, suponha que você tenha uma classe Point para representar um ponto 2D:
namespace PhpCommon Comparison Equatable ;
final class Point implements Equatable
{
private $ x ;
private $ y ;
public function __construct ( $ x , $ y )
{
$ this -> x = ( int ) $ x ;
$ this -> y = ( int ) $ y ;
}
public function equals ( Equatable $ point )
{
if (! $ point instanceof Point) {
return false ;
}
return $ this -> x === $ point -> x && $ this -> y === $ point -> y ;
}
}
Um Point
contém as coordenadas x e y de um ponto. De acordo com a definição da classe, dois pontos são considerados iguais se possuírem as mesmas coordenadas. Entretanto, se você pretende armazenar instâncias de Point
em um mapa baseado em hash, por exemplo, porque deseja associar coordenadas a rótulos, então você deve garantir que sua classe produza códigos hash que sejam coerentes com a lógica usada para determinar quando dois os pontos são considerados iguais:
namespace PhpCommon Comparison Equatable ;
final class Point implements Hashable
{
private $ x ;
private $ y ;
public function __construct ( $ x , $ y )
{
$ this -> x = ( int ) $ x ;
$ this -> y = ( int ) $ y ;
}
public function equals ( Equatable $ point )
{
if (! $ point instanceof Point) {
return false ;
}
return $ this -> x === $ point -> x && $ this -> y === $ point -> y ;
}
public function getHash ()
{
return 37 * ( 31 + $ this -> $ x ) + $ this -> $ x ;
}
}
Dessa forma, o método getHash()
funciona de acordo com o método equals()
, embora o algoritmo de hashing possa não ser ideal. A implementação de algoritmos eficientes para geração de código hash está além do escopo deste guia. No entanto, é recomendado usar um algoritmo rápido que produza resultados razoavelmente diferentes para valores desiguais e mudar a lógica de comparação pesada para Equatable::equals()
.
Observe que objetos hasháveis devem ser imutáveis ou você precisa exercer disciplina para não alterá-los depois de terem sido usados em estruturas baseadas em hash.
Um Hasher fornece funcionalidade de hash para tipos primitivos e objetos de classes que não implementam Hashable
.
O método hash()
introduzido por esta interface tem como objetivo fornecer um meio para realizar verificações rápidas de inequivalência e inserção e pesquisa eficientes em estruturas de dados baseadas em hash. Este método é sempre coerente com equivalent()
, o que significa que para quaisquer referências $x
e $y
, se equivalent($x, $y)
, então hash($x) === hash($y)
. No entanto, se equivalence($x, $y)
for avaliado como false
, hash($x) === hash($y)
ainda pode ser verdadeiro. Daí porque o método hash()
é adequado para verificações de inequivalência , mas não para verificações de equivalência .
Todas as implementações de Equivalence
incluídas nesta biblioteca também fornecem funcionalidade de hashing. Mais informações sobre como os valores são hash podem ser encontradas na documentação da respectiva implementação.
Seguindo a mesma lógica dos conceitos discutidos anteriormente, Comparable
e Comparator
são interfaces para fornecer estratégias de ordenação natural e customizada, respectivamente. Ambas as interfaces especificam uma relação de ordem total, uma relação que é reflexiva , antissimétrica e transitiva .
Esta interface impõe uma ordenação total aos objetos de cada classe que a implementa. Essa ordem é chamada de ordem natural da classe, e o método Comparable::compareTo()
é chamado de método de comparação natural .
O exemplo a seguir mostra como uma classe pode definir a ordem natural de suas instâncias:
use PhpCommon Comparison UnexpectedTypeException ;
final class BigInteger implements Comparable
{
private $ value ;
public function __construct ( $ value )
{
$ this -> value = ( string ) $ value ;
}
public function compareTo ( Comparable $ other )
{
if (! $ other instanceof self) {
throw UnexpectedTypeException:: forType (BigInteger::class, $ other );
}
return bccomp ( $ this -> value , $ other -> value );
}
}
O objetivo de um Comparator
é permitir definir uma ou mais estratégias de comparação que não sejam a estratégia de comparação natural para uma classe. Idealmente, um Comparator
deve ser implementado por uma classe diferente daquela para a qual define a estratégia de comparação. Se você deseja definir uma estratégia de comparação natural para uma classe, você pode implementar Comparable
.
Comparadores podem ser passados para um método de classificação de uma coleção para permitir controle preciso sobre sua ordem de classificação. Também pode ser usado para controlar a ordem de certas estruturas de dados, como conjuntos ordenados ou mapas ordenados. Por exemplo, considere o seguinte comparador que ordena as strings de acordo com seu comprimento:
use PhpCommon Comparison Comparator ;
class StringLengthComparator implements Comparator
{
public function compare ( $ left , $ right )
{
return strlen ( $ left ) <=> strlen ( $ right );
}
}
$ comparator = new StringLengthComparator ();
// Outputs int(-1)
var_dump ( $ comparator -> compare ( ' ab ' , ' a ' ));
Esta implementação representa uma das muitas maneiras possíveis de classificar strings. Outras estratégias incluem classificação alfabética, lexicográfica, etc.
Consulte CHANGELOG para obter mais informações sobre o que mudou recentemente.
$ composer test
Confira a documentação do teste para obter mais detalhes.
Contribuições para o pacote são sempre bem-vindas!
Consulte CONTRIBUIÇÃO e CONDUTA para obter detalhes.
Se você descobrir algum problema relacionado à segurança, envie um e-mail para [email protected] em vez de usar o rastreador de problemas.
Todo o conteúdo deste pacote está licenciado sob a licença MIT.