├── .gitignore ├── demo ├── src │ ├── MyValueType.php │ ├── PostCode.php │ ├── AddressBuilder.php │ ├── Command.php │ └── Address.php └── auto │ ├── AutoValue_AddressBuilder.php │ ├── AutoValue_Command.php │ └── AutoValue_Address.php ├── src ├── ValueToString │ └── ToStringMethodProcessor.php ├── MethodGenerator.php ├── PropertyInferrer.php ├── AutoClassType.php ├── ValueClass │ ├── MethodProcessor.php │ ├── ValueClassType.php │ ├── ValueClassGenerator.php │ ├── MethodProcessorList.php │ └── AccessorMethodProcessor.php ├── MethodDefinition.php ├── TemplateClass.php ├── MethodGeneratorList.php ├── BuilderClass │ ├── BuilderClassGenerator.php │ ├── BuildMethodGenerator.php │ ├── SetterMethodGenerator.php │ └── BuilderClassType.php ├── MethodDefinitionCollection.php ├── PropertyCollection.php ├── ValueToArray │ └── ToArrayMethodProcessor.php ├── Property.php ├── ValueWither │ └── WitherMethodProcessor.php ├── Memoize │ └── MemoizeMethodProcessor.php ├── Console │ └── BuildCommand.php ├── TemplateClassLocator.php ├── ValueToBuilder │ └── ToBuilderMethodProcessor.php ├── ReflectionMethodCollection.php ├── functions.php ├── TemplateDirectoryProcessor.php └── ValueEquals │ └── EqualsMethodProcessor.php ├── tests └── Console │ └── Build │ ├── templates │ ├── composer.json │ ├── MyValueType.php │ ├── PostCode.php │ ├── vendor │ │ ├── autoload.php │ │ └── composer │ │ │ ├── autoload_classmap.php │ │ │ ├── autoload_namespaces.php │ │ │ ├── autoload_psr4.php │ │ │ ├── autoload_static.php │ │ │ ├── LICENSE │ │ │ ├── autoload_real.php │ │ │ └── ClassLoader.php │ ├── AddressBuilder.php │ ├── Command.php │ └── Address.php │ └── BuildTest.php ├── .travis.yml ├── phpunit.xml ├── docs ├── why.md ├── generated-example.md ├── generated-builder-example.md ├── practices.md ├── builders.md ├── index.md ├── howto.md └── builders-howto.md ├── composer.json ├── README.md └── bin └── auto /.gitignore: -------------------------------------------------------------------------------- 1 | .idea 2 | /vendor 3 | /composer.lock 4 | -------------------------------------------------------------------------------- /demo/src/MyValueType.php: -------------------------------------------------------------------------------- 1 | array($baseDir . '/'), 10 | ); 11 | -------------------------------------------------------------------------------- /src/MethodGenerator.php: -------------------------------------------------------------------------------- 1 | 6 | */ 7 | interface MethodGenerator 8 | { 9 | public function generateMethods(ReflectionMethodCollection $matchedMethods, PropertyCollection $properties): MethodDefinitionCollection; 10 | } -------------------------------------------------------------------------------- /src/PropertyInferrer.php: -------------------------------------------------------------------------------- 1 | 8 | */ 9 | interface PropertyInferrer 10 | { 11 | public function inferProperties(ClassReflector $reflector, string $templateValueClassName): PropertyCollection; 12 | } -------------------------------------------------------------------------------- /phpunit.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | ./tests 5 | 6 | 7 | 8 | 9 | src 10 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /src/AutoClassType.php: -------------------------------------------------------------------------------- 1 | 8 | */ 9 | interface AutoClassType 10 | { 11 | public function annotation(): string; 12 | 13 | public function generateAutoClass(ClassReflector $reflector, string $templateClassName): string; 14 | } 15 | -------------------------------------------------------------------------------- /demo/src/AddressBuilder.php: -------------------------------------------------------------------------------- 1 | $name, 13 | 'payload' => $payload, 14 | 'timestamp' => new \DateTimeImmutable(), 15 | ]); 16 | } 17 | 18 | public abstract function name(): string; 19 | 20 | public abstract function payload(); 21 | 22 | public abstract function timestamp(): \DateTimeImmutable; 23 | 24 | public abstract function equals($subject): bool; 25 | } -------------------------------------------------------------------------------- /src/ValueClass/MethodProcessor.php: -------------------------------------------------------------------------------- 1 | 10 | */ 11 | abstract class MethodProcessor implements MethodGenerator 12 | { 13 | /** 14 | * @return string[] Names of matched methods 15 | */ 16 | public abstract function matchMethods(ReflectionMethodCollection $methods): array; 17 | 18 | public function inferProperties(ReflectionMethodCollection $matchedMethods): PropertyCollection 19 | { 20 | return PropertyCollection::create(); 21 | } 22 | } -------------------------------------------------------------------------------- /tests/Console/Build/templates/Command.php: -------------------------------------------------------------------------------- 1 | $name, 13 | 'payload' => $payload, 14 | 'timestamp' => new \DateTimeImmutable(), 15 | ]); 16 | } 17 | 18 | public abstract function name(): string; 19 | 20 | public abstract function payload(); 21 | 22 | public abstract function withPayload($payload): self; 23 | 24 | public abstract function timestamp(): \DateTimeImmutable; 25 | 26 | public abstract function equals($subject): bool; 27 | } -------------------------------------------------------------------------------- /docs/why.md: -------------------------------------------------------------------------------- 1 | # Why use AutoValue? 2 | 3 | AutoValue is the only solution to the value class problem in PHP having all of 4 | the following characteristics: 5 | 6 | * **API-invisible** (callers cannot become dependent on your choice to use it) 7 | * No runtime dependencies 8 | * Negligible cost to performance 9 | * Very few limitations on what your class can do 10 | * Extralinguistic "magic" kept to an absolute minimum (uses only standard PHP 11 | technologies) 12 | 13 | This [slide presentation] from the authors of the original AutoValue package for 14 | Java compares AutoValue to numerous alternatives and explains why they think it 15 | is better. 16 | 17 | [slide presentation]: https://docs.google.com/presentation/d/14u_h-lMn7f1rXE1nDiLX0azS3IkgjGl5uxp5jGJ75RE/edit 18 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "space48/auto-value", 3 | "type": "lib", 4 | "description": "Generated immutable value classes for PHP7.1+", 5 | "keywords": [ 6 | "value", 7 | "value objects", 8 | "ddd" 9 | ], 10 | "license": "MIT", 11 | "authors": [ 12 | { 13 | "name": "Josh Di Fabio", 14 | "email": "joshdifabio@gmail.com" 15 | } 16 | ], 17 | "require": { 18 | "php": "~8.1", 19 | "roave/better-reflection": "*", 20 | "symfony/console": "*" 21 | }, 22 | "require-dev": { 23 | "phpunit/phpunit": "^7.2" 24 | }, 25 | "autoload": { 26 | "psr-4": { 27 | "AutoValue\\": "src" 28 | }, 29 | "files": [ "src/functions.php" ] 30 | }, 31 | "autoload-dev": { 32 | "psr-4": { 33 | "AutoValue\\Demo\\": ["demo/src", "demo/auto"] 34 | } 35 | }, 36 | "bin": ["bin/auto"] 37 | } 38 | -------------------------------------------------------------------------------- /tests/Console/Build/templates/vendor/composer/autoload_static.php: -------------------------------------------------------------------------------- 1 | 11 | array ( 12 | 'MyTemplates\\' => 12, 13 | ), 14 | ); 15 | 16 | public static $prefixDirsPsr4 = array ( 17 | 'MyTemplates\\' => 18 | array ( 19 | 0 => __DIR__ . '/../..' . '/', 20 | ), 21 | ); 22 | 23 | public static function getInitializer(ClassLoader $loader) 24 | { 25 | return \Closure::bind(function () use ($loader) { 26 | $loader->prefixLengthsPsr4 = ComposerStaticInitfde5e053bc6b07bb25d2af241edcb63e::$prefixLengthsPsr4; 27 | $loader->prefixDirsPsr4 = ComposerStaticInitfde5e053bc6b07bb25d2af241edcb63e::$prefixDirsPsr4; 28 | 29 | }, null, ClassLoader::class); 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /src/MethodDefinition.php: -------------------------------------------------------------------------------- 1 | 8 | */ 9 | class MethodDefinition 10 | { 11 | public static function of(ReflectionMethod $reflection, string $body): self 12 | { 13 | $methodDefinition = new self; 14 | $methodDefinition->reflection = $reflection; 15 | $methodDefinition->body = $body; 16 | return $methodDefinition; 17 | } 18 | 19 | public function reflection(): ReflectionMethod 20 | { 21 | return $this->reflection; 22 | } 23 | 24 | public function name(): string 25 | { 26 | return $this->reflection->getShortName(); 27 | } 28 | 29 | public function body(): string 30 | { 31 | return $this->body; 32 | } 33 | 34 | /** @var ReflectionMethod */ 35 | private $reflection; 36 | private $body; 37 | 38 | private function __construct() 39 | { 40 | } 41 | } -------------------------------------------------------------------------------- /demo/src/Address.php: -------------------------------------------------------------------------------- 1 | lines()); 41 | } 42 | } -------------------------------------------------------------------------------- /src/TemplateClass.php: -------------------------------------------------------------------------------- 1 | 6 | */ 7 | class TemplateClass 8 | { 9 | public static function of(string $className, string $relativeFilePath, array $annotations): self 10 | { 11 | $templateClass = new self; 12 | $templateClass->className = $className; 13 | $templateClass->relativeFilePath = $relativeFilePath; 14 | $templateClass->annotations = $annotations; 15 | return $templateClass; 16 | } 17 | 18 | public function className(): string 19 | { 20 | return $this->className; 21 | } 22 | 23 | public function relativeFilePath(): string 24 | { 25 | return $this->relativeFilePath; 26 | } 27 | 28 | public function annotations(): array 29 | { 30 | return $this->annotations; 31 | } 32 | 33 | private $className; 34 | private $relativeFilePath; 35 | private $annotations; 36 | 37 | private function __construct() 38 | { 39 | 40 | } 41 | } -------------------------------------------------------------------------------- /tests/Console/Build/templates/Address.php: -------------------------------------------------------------------------------- 1 | lines()[0] ?? null; 24 | } 25 | 26 | public abstract function city(): ?string; 27 | 28 | public abstract function country(): string; 29 | 30 | public abstract function withCountry(string $country): self; 31 | 32 | public abstract function postCode(): PostCode; 33 | 34 | public abstract function metadata(); 35 | 36 | public abstract function foo(); 37 | 38 | private $n = 0; 39 | 40 | /** @Memoized */ 41 | public function n(): int 42 | { 43 | return $this->n++; 44 | } 45 | } -------------------------------------------------------------------------------- /tests/Console/Build/templates/vendor/composer/LICENSE: -------------------------------------------------------------------------------- 1 | 2 | Copyright (c) Nils Adermann, Jordi Boggiano 3 | 4 | Permission is hereby granted, free of charge, to any person obtaining a copy 5 | of this software and associated documentation files (the "Software"), to deal 6 | in the Software without restriction, including without limitation the rights 7 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 8 | copies of the Software, and to permit persons to whom the Software is furnished 9 | to do so, subject to the following conditions: 10 | 11 | The above copyright notice and this permission notice shall be included in all 12 | copies or substantial portions of the Software. 13 | 14 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 15 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 16 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 17 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 18 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 19 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 20 | THE SOFTWARE. 21 | 22 | -------------------------------------------------------------------------------- /docs/generated-example.md: -------------------------------------------------------------------------------- 1 | # Generated example 2 | 3 | 4 | For the code shown in the [introduction](index.md), the following is typical 5 | code AutoValue might generate: 6 | 7 | ```php 8 | 9 | /** 10 | * @internal 11 | */ 12 | final class AutoValue_Animal extends Animal 13 | { 14 | /** @var string */ 15 | private $name; 16 | /** @var int */ 17 | private $numberOfLegs; 18 | 19 | protected function __construct(array $propertyValues = []) 20 | { 21 | foreach ($propertyValues as $property => $value) { 22 | $this->$property = $value; 23 | } 24 | } 25 | 26 | public function equals($value): bool 27 | { 28 | return $value instanceof self 29 | && $this->name === $value->name 30 | && $this->numberOfLegs === $value->numberOfLegs; 31 | } 32 | 33 | public function name(): string 34 | { 35 | return $this->name; 36 | } 37 | 38 | public function numberOfLegs(): int 39 | { 40 | return $this->numberOfLegs; 41 | } 42 | 43 | /** 44 | * @internal 45 | */ 46 | public static function ___withTrustedValues(array $propertyValues): self 47 | { 48 | return new self($propertyValues); 49 | } 50 | } 51 | 52 | ``` -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # AutoValue PHP 2 | 3 | *Generated immutable value classes for PHP7.1+* 4 | 5 | [![Build Status](https://travis-ci.org/Space48/auto-value-php.svg?branch=master)](https://travis-ci.org/Space48/auto-value-php) 6 | 7 | *AutoValue PHP is a port of [Google AutoValue] (Kevin Bourrillion, Éamonn 8 | McManus) from Java to PHP.* 9 | 10 | **Value classes** are increasingly common in PHP projects. These are classes for 11 | which you want to treat any two instances with suitably equal field values as 12 | interchangeable. 13 | 14 | Writing these classes by hand the first time is not too bad, with the aid of a 15 | few helper methods and IDE templates. But once written they continue to burden 16 | reviewers, editors and future readers. Their wide expanses of boilerplate 17 | sharply decrease the signal-to-noise ratio of your code... and they love to 18 | harbor hard-to-spot bugs. 19 | 20 | AutoValue provides an easier way to create immutable value classes, with a lot 21 | less code and less room for error, while **not restricting your freedom** to 22 | code almost any aspect of your class exactly the way you want it. 23 | 24 | For more information, consult the [detailed documentation](docs/index.md). 25 | 26 | [Google AutoValue]: https://github.com/google/auto/blob/master/value 27 | -------------------------------------------------------------------------------- /src/MethodGeneratorList.php: -------------------------------------------------------------------------------- 1 | 6 | */ 7 | class MethodGeneratorList 8 | { 9 | private $methodGenerators = []; 10 | 11 | /** 12 | * @param MethodGenerator[] $methodGenerators 13 | */ 14 | public function __construct(array $methodGenerators = []) 15 | { 16 | $this->methodGenerators = $methodGenerators; 17 | } 18 | 19 | public function generateMethods(ReflectionMethodCollection $methods, PropertyCollection $properties): MethodDefinitionCollection 20 | { 21 | $unprocessedMethods = $methods; 22 | $methodDefinitions = MethodDefinitionCollection::create(); 23 | foreach ($this->methodGenerators as $methodGenerator) { 24 | $_methodDefinitions = $methodGenerator->generateMethods($unprocessedMethods, $properties); 25 | $methodDefinitions = $methodDefinitions->plus($_methodDefinitions); 26 | $unprocessedMethods = $unprocessedMethods->withoutMethods($_methodDefinitions->methodNames()); 27 | } 28 | if (!$unprocessedMethods->filterAbstract()->isEmpty()) { 29 | throw new \Exception('Some abstract methods could not be processed.'); 30 | } 31 | return $methodDefinitions; 32 | } 33 | } -------------------------------------------------------------------------------- /src/BuilderClass/BuilderClassGenerator.php: -------------------------------------------------------------------------------- 1 | 11 | */ 12 | class BuilderClassGenerator 13 | { 14 | public function generateClass(ReflectionClass $templateClass, MethodDefinitionCollection $methodDefinitions): string 15 | { 16 | $methodDeclarations = \implode("\n\n", $methodDefinitions->map(function (MethodDefinition $methodDefinition) { 17 | return generateConcreteMethod($methodDefinition->reflection(), $methodDefinition->body()); 18 | })); 19 | 20 | return <<getNamespaceName()}; 22 | 23 | /** 24 | * @internal 25 | */ 26 | final class AutoValue_{$templateClass->getShortName()} extends {$templateClass->getShortName()} 27 | { 28 | private \$propertyValues = []; 29 | 30 | $methodDeclarations 31 | 32 | /** 33 | * @internal 34 | */ 35 | public static function ___withTrustedValues(array \$propertyValues): self 36 | { 37 | \$builder = new self; 38 | \$builder->propertyValues = \$propertyValues; 39 | return \$builder; 40 | } 41 | } 42 | THEPHP; 43 | } 44 | } -------------------------------------------------------------------------------- /src/MethodDefinitionCollection.php: -------------------------------------------------------------------------------- 1 | 6 | */ 7 | class MethodDefinitionCollection 8 | { 9 | public static function create(): self 10 | { 11 | return new self; 12 | } 13 | 14 | public function methodNames(): array 15 | { 16 | return \array_keys($this->items); 17 | } 18 | 19 | public function withAdditionalMethodDefinition(MethodDefinition $methodDefinition): self 20 | { 21 | if (isset($this->items[$methodDefinition->name()])) { 22 | throw new \Exception('Multiple definitions provided for method ' . $methodDefinition->name() . '.'); 23 | } 24 | 25 | $result = clone $this; 26 | $result->items[$methodDefinition->name()] = $methodDefinition; 27 | return $result; 28 | } 29 | 30 | public function plus(self $methodDefinitions): self 31 | { 32 | $result = $this; 33 | 34 | foreach ($methodDefinitions->items as $item) { 35 | $result = $result->withAdditionalMethodDefinition($item); 36 | } 37 | 38 | return $result; 39 | } 40 | 41 | public function map(callable $fn): array 42 | { 43 | return \array_map($fn, $this->items); 44 | } 45 | 46 | /** @var MethodDefinition[] */ 47 | private $items = []; 48 | 49 | private function __construct() 50 | { 51 | } 52 | } -------------------------------------------------------------------------------- /bin/auto: -------------------------------------------------------------------------------- 1 | #!/usr/bin/php 2 | astLocator(); 23 | $classLocator = new TemplateClassLocator($astLocator); 24 | $classReflector = new ClassReflector(new ComposerSourceLocator($classLoader, $astLocator)); 25 | $valueClassType = ValueClassType::withDefaultConfiguration(); 26 | $autoClassTypes = [$valueClassType, BuilderClassType::withDefaultConfiguration($valueClassType)]; 27 | $templateDirectoryProcessor = new TemplateDirectoryProcessor($astLocator, $classLocator, $autoClassTypes, new PhpInternalSourceLocator($astLocator)); 28 | $buildCommand = new BuildCommand($templateDirectoryProcessor); 29 | $application = new \Symfony\Component\Console\Application(); 30 | $application->add($buildCommand); 31 | 32 | $application->run(); 33 | 34 | function getAutoloader(array $candidateVendorDirs) { 35 | foreach ($candidateVendorDirs as $dir) { 36 | $filePath = $dir . \DIRECTORY_SEPARATOR . 'autoload.php'; 37 | if (\file_exists($filePath)) { 38 | return require $filePath; 39 | } 40 | } 41 | } -------------------------------------------------------------------------------- /src/PropertyCollection.php: -------------------------------------------------------------------------------- 1 | 6 | */ 7 | class PropertyCollection 8 | { 9 | public static function create(): self 10 | { 11 | return new self; 12 | } 13 | 14 | public function propertyNames(): array 15 | { 16 | return \array_keys($this->properties); 17 | } 18 | 19 | public function getProperty(string $name): ?Property 20 | { 21 | return $this->properties[$name]; 22 | } 23 | 24 | public function withProperty(Property $property): self 25 | { 26 | $result = clone $this; 27 | $result->properties[$property->name()] = $property; 28 | return $result; 29 | } 30 | 31 | public function plus(self $collection): self 32 | { 33 | $result = clone $this; 34 | foreach ($collection->properties as $name => $property) { 35 | $result->properties[$name] = isset($result->properties[$name]) 36 | ? $result->properties[$name]->withAdditionalConstraints($property->constraints()) 37 | : $property; 38 | } 39 | return $result; 40 | } 41 | 42 | public function filter(callable $predicate): self 43 | { 44 | $result = new self; 45 | $result->properties = \array_filter($this->properties, $predicate); 46 | return $result; 47 | } 48 | 49 | public function map(callable $fn): array 50 | { 51 | return \array_map($fn, \array_values($this->properties)); 52 | } 53 | 54 | public function mapPropertyNames(callable $fn): array 55 | { 56 | return \array_map($fn, \array_keys($this->properties)); 57 | } 58 | 59 | private $properties = []; 60 | 61 | private function __construct() 62 | { 63 | } 64 | } -------------------------------------------------------------------------------- /demo/auto/AutoValue_AddressBuilder.php: -------------------------------------------------------------------------------- 1 | propertyValues['lines'] = $lines; 14 | return $this; 15 | } 16 | 17 | public function setCity(string $city): \AutoValue\Demo\AddressBuilder 18 | { 19 | $this->propertyValues['city'] = $city; 20 | return $this; 21 | } 22 | 23 | public function setCountry(string $country): \AutoValue\Demo\AddressBuilder 24 | { 25 | $this->propertyValues['country'] = $country; 26 | return $this; 27 | } 28 | 29 | public function setPostCode(\AutoValue\Demo\PostCode $postCode): \AutoValue\Demo\AddressBuilder 30 | { 31 | $this->propertyValues['postCode'] = $postCode; 32 | return $this; 33 | } 34 | 35 | public function setMetadata($metadata): \AutoValue\Demo\AddressBuilder 36 | { 37 | $this->propertyValues['metadata'] = $metadata; 38 | return $this; 39 | } 40 | 41 | public function build(): \AutoValue\Demo\Address 42 | { 43 | foreach (['lines', 'country', 'postCode'] as $property) { 44 | if (!isset($this->propertyValues[$property])) { 45 | throw new \Exception("Required property $property not initialized."); 46 | } 47 | } 48 | return AutoValue_Address::___withTrustedValues($this->propertyValues); 49 | } 50 | 51 | /** 52 | * @internal 53 | */ 54 | public static function ___withTrustedValues(array $propertyValues): self 55 | { 56 | $builder = new self; 57 | $builder->propertyValues = $propertyValues; 58 | return $builder; 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /src/ValueToArray/ToArrayMethodProcessor.php: -------------------------------------------------------------------------------- 1 | 13 | */ 14 | class ToArrayMethodProcessor extends MethodProcessor 15 | { 16 | public function matchMethods(ReflectionMethodCollection $methods): array 17 | { 18 | return $methods 19 | ->filterAbstract() 20 | ->filter(function (ReflectionMethod $reflectionMethod) { 21 | return $reflectionMethod->getName() === 'toArray' 22 | && $reflectionMethod->getNumberOfParameters() === 0 23 | && ($returnType = $reflectionMethod->getReturnType()) 24 | && (string)$returnType === 'array'; 25 | }) 26 | ->methodNames(); 27 | } 28 | 29 | public function generateMethods(ReflectionMethodCollection $matchedMethods, PropertyCollection $properties): MethodDefinitionCollection 30 | { 31 | return $matchedMethods->reduce(MethodDefinitionCollection::create(), function (MethodDefinitionCollection $methodDefinitions, ReflectionMethod $method) use ($properties) { 32 | $propertyReads = \implode($properties->mapPropertyNames(function (string $propertyName) { 33 | return "\n '$propertyName' => \$this->$propertyName,"; 34 | })); 35 | $methodBody = <<withAdditionalMethodDefinition(MethodDefinition::of($method, $methodBody)); 40 | }); 41 | } 42 | } -------------------------------------------------------------------------------- /src/Property.php: -------------------------------------------------------------------------------- 1 | 11 | */ 12 | class Property 13 | { 14 | public static function fromAccessorMethod(string $propertyName, ReflectionMethod $accessorMethod): self 15 | { 16 | $property = new self; 17 | $property->name = $propertyName; 18 | $property->accessorMethod = $accessorMethod; 19 | return $property; 20 | } 21 | 22 | public function name(): string 23 | { 24 | return $this->name; 25 | } 26 | 27 | public function phpType(): ?ReflectionType 28 | { 29 | if (!$this->phpType) { 30 | $this->phpType = $this->accessorMethod->getReturnType(); 31 | } 32 | return $this->phpType; 33 | } 34 | 35 | public function docBlockType(): string 36 | { 37 | if (!isset($this->docBlockType)) { 38 | $docBlockTypes = $this->accessorMethod->getDocBlockReturnTypes(); 39 | $this->docBlockType = $docBlockTypes 40 | ? \implode('|', $docBlockTypes) 41 | : ($this->phpType() 42 | ? generateTypeHint($this->phpType(), $this->accessorMethod->getDeclaringClass()) 43 | : 'mixed' 44 | ); 45 | } 46 | return $this->docBlockType; 47 | } 48 | 49 | public function isRequired(): bool 50 | { 51 | return $this->phpType() && !$this->phpType()->allowsNull(); 52 | } 53 | 54 | private $name; 55 | /** @var ReflectionMethod */ 56 | private $accessorMethod; 57 | /** @var ReflectionType|null */ 58 | private $phpType; 59 | 60 | private function __construct() 61 | { 62 | } 63 | } -------------------------------------------------------------------------------- /demo/auto/AutoValue_Command.php: -------------------------------------------------------------------------------- 1 | $value) { 19 | $this->$property = $value; 20 | } 21 | } 22 | 23 | public function equals($subject): bool 24 | { 25 | $typedPropertiesAreEqual = $subject instanceof self 26 | && $this->name === $subject->name 27 | && $this->timestamp == $subject->timestamp; 28 | if (!$typedPropertiesAreEqual) { 29 | return false; 30 | } 31 | $compareValues = static function ($value1, $value2) use (&$compareValues) { 32 | if (\is_array($value1)) { 33 | $equal = \is_array($value2) && \count($value1) === \count($value2) && !\array_udiff_assoc($value1, $value2, $compareValues); 34 | } else { 35 | $equal = $value1 === $value2 || (\method_exists($value1, 'equals') ? $value1->equals($value2) : \is_object($value1) && $value1 == $value2); 36 | } 37 | return $equal ? 0 : 1; 38 | }; 39 | return $compareValues($this->payload, $subject->payload) === 0; 40 | } 41 | 42 | public function name(): string 43 | { 44 | return $this->name; 45 | } 46 | 47 | public function payload() 48 | { 49 | return $this->payload; 50 | } 51 | 52 | public function timestamp(): \DateTimeImmutable 53 | { 54 | return $this->timestamp; 55 | } 56 | 57 | /** 58 | * @internal 59 | */ 60 | public static function ___withTrustedValues(array $propertyValues): self 61 | { 62 | return new self($propertyValues); 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /tests/Console/Build/templates/vendor/composer/autoload_real.php: -------------------------------------------------------------------------------- 1 | = 50600 && !defined('HHVM_VERSION') && (!function_exists('zend_loader_file_encoded') || !zend_loader_file_encoded()); 27 | if ($useStaticLoader) { 28 | require_once __DIR__ . '/autoload_static.php'; 29 | 30 | call_user_func(\Composer\Autoload\ComposerStaticInitfde5e053bc6b07bb25d2af241edcb63e::getInitializer($loader)); 31 | } else { 32 | $map = require __DIR__ . '/autoload_namespaces.php'; 33 | foreach ($map as $namespace => $path) { 34 | $loader->set($namespace, $path); 35 | } 36 | 37 | $map = require __DIR__ . '/autoload_psr4.php'; 38 | foreach ($map as $namespace => $path) { 39 | $loader->setPsr4($namespace, $path); 40 | } 41 | 42 | $classMap = require __DIR__ . '/autoload_classmap.php'; 43 | if ($classMap) { 44 | $loader->addClassMap($classMap); 45 | } 46 | } 47 | 48 | $loader->register(true); 49 | 50 | return $loader; 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /src/ValueWither/WitherMethodProcessor.php: -------------------------------------------------------------------------------- 1 | 16 | */ 17 | class WitherMethodProcessor extends MethodProcessor 18 | { 19 | public function matchMethods(ReflectionMethodCollection $methods): array 20 | { 21 | return $methods 22 | ->filterAbstract() 23 | ->filter(function (ReflectionMethod $reflectionMethod) { 24 | return getPropertyName($reflectionMethod->getShortName(), 'with') !== null 25 | && $reflectionMethod->getNumberOfParameters() === 1 26 | && ($returnType = $reflectionMethod->getReturnType()) 27 | && isClass($returnType) 28 | && getClass($reflectionMethod->getDeclaringClass(), $returnType)->getName() === $reflectionMethod->getDeclaringClass()->getName(); 29 | }) 30 | ->methodNames(); 31 | } 32 | 33 | public function generateMethods(ReflectionMethodCollection $matchedMethods, PropertyCollection $properties): MethodDefinitionCollection 34 | { 35 | return $matchedMethods->reduce(MethodDefinitionCollection::create(), function (MethodDefinitionCollection $methodDefinitions, ReflectionMethod $method) { 36 | $propertyName = getPropertyName($method->getShortName(), 'with'); 37 | $parameterName = $method->getParameters()[0]->getName(); 38 | $methodBody = <<__memoized); 41 | \$result->$propertyName = \${$parameterName}; 42 | return \$result; 43 | THEPHP; 44 | return $methodDefinitions->withAdditionalMethodDefinition(MethodDefinition::of($method, $methodBody)); 45 | }); 46 | } 47 | } -------------------------------------------------------------------------------- /src/BuilderClass/BuildMethodGenerator.php: -------------------------------------------------------------------------------- 1 | 14 | */ 15 | class BuildMethodGenerator implements MethodGenerator 16 | { 17 | public function generateMethods(ReflectionMethodCollection $matchedMethods, PropertyCollection $properties): MethodDefinitionCollection 18 | { 19 | return $matchedMethods 20 | ->filterAbstract() 21 | ->filter(self::abstractMethodMatcher()) 22 | ->reduce(MethodDefinitionCollection::create(), function (MethodDefinitionCollection $methodDefinitions, ReflectionMethod $method) use ($properties) { 23 | $valueTemplateClass = $method->getReturnType()->targetReflectionClass()->getShortName(); 24 | $valueAutoClass = "AutoValue_$valueTemplateClass"; 25 | $methodBody = <<propertyValues); 27 | THEPHP; 28 | return $methodDefinitions->withAdditionalMethodDefinition(MethodDefinition::of($method, $methodBody)); 29 | }); 30 | } 31 | 32 | private static function abstractMethodMatcher(): callable 33 | { 34 | return function (ReflectionMethod $reflectionMethod) { 35 | if ($reflectionMethod->getNumberOfParameters() !== 0) { 36 | return false; 37 | } 38 | if (!$reflectionMethod->hasReturnType()) { 39 | return false; 40 | } 41 | $returnType = $reflectionMethod->getReturnType(); 42 | if ($returnType->isBuiltin()) { 43 | return false; 44 | } 45 | $valueClassName = BuilderClassType::getValueClass($reflectionMethod->getDeclaringClass()->getName()); 46 | return $returnType->targetReflectionClass()->getName() === $valueClassName; 47 | }; 48 | } 49 | } -------------------------------------------------------------------------------- /docs/generated-builder-example.md: -------------------------------------------------------------------------------- 1 | # Generated builder example 2 | 3 | 4 | For the code shown in the [builder documentation](builders.md), the following is 5 | typical code AutoValue might generate: 6 | 7 | ```php 8 | /** 9 | * @internal 10 | */ 11 | final class AutoValue_Animal extends Animal 12 | { 13 | /** @var string */ 14 | private $name; 15 | /** @var int */ 16 | private $numberOfLegs; 17 | 18 | protected function __construct(array $propertyValues = []) 19 | { 20 | foreach ($propertyValues as $property => $value) { 21 | $this->$property = $value; 22 | } 23 | } 24 | 25 | public function name(): string 26 | { 27 | return $this->name; 28 | } 29 | 30 | public function numberOfLegs(): int 31 | { 32 | return $this->numberOfLegs; 33 | } 34 | 35 | /** 36 | * @internal 37 | */ 38 | public static function ___withTrustedValues(array $propertyValues): self 39 | { 40 | return new self($propertyValues); 41 | } 42 | } 43 | 44 | /** 45 | * @internal 46 | */ 47 | final class AutoValue_AnimalBuilder extends AnimalBuilder 48 | { 49 | private $propertyValues = []; 50 | 51 | public function name(string $value): \AutoValue\Demo\AnimalBuilder 52 | { 53 | $this->propertyValues['name'] = $value; 54 | return $this; 55 | } 56 | 57 | public function numberOfLegs(int $value): \AutoValue\Demo\AnimalBuilder 58 | { 59 | $this->propertyValues['numberOfLegs'] = $value; 60 | return $this; 61 | } 62 | 63 | public function build(): \AutoValue\Demo\Animal 64 | { 65 | foreach (['name', 'numberOfLegs'] as $property) { 66 | if (!isset($this->propertyValues[$property])) { 67 | throw new \Exception("Required property $property not initialized."); 68 | } 69 | } 70 | return AutoValue_Animal::___withTrustedValues($this->propertyValues); 71 | } 72 | 73 | /** 74 | * @internal 75 | */ 76 | public static function ___withTrustedValues(array $propertyValues): self 77 | { 78 | $builder = new self; 79 | $builder->propertyValues = $propertyValues; 80 | return $builder; 81 | } 82 | } 83 | ``` -------------------------------------------------------------------------------- /src/Memoize/MemoizeMethodProcessor.php: -------------------------------------------------------------------------------- 1 | 13 | */ 14 | class MemoizeMethodProcessor extends MethodProcessor 15 | { 16 | public function matchMethods(ReflectionMethodCollection $methods): array 17 | { 18 | return $methods 19 | ->filterConcrete() 20 | ->filter(function (ReflectionMethod $reflectionMethod) { 21 | return preg_match('{\*\s+\@Memoize(d)?\s}m', $reflectionMethod->getDocComment()) > 0; 22 | }) 23 | ->filter(function (ReflectionMethod $reflectionMethod) { 24 | return $reflectionMethod->getNumberOfParameters() === 0; 25 | }) 26 | ->methodNames(); 27 | } 28 | 29 | public function generateMethods(ReflectionMethodCollection $matchedMethods, PropertyCollection $properties): MethodDefinitionCollection 30 | { 31 | return $matchedMethods->reduce( 32 | MethodDefinitionCollection::create(), 33 | function (MethodDefinitionCollection $methodDefinitions, ReflectionMethod $method) { 34 | $allowsNull = !$method->hasReturnType() || $method->getReturnType()->allowsNull(); 35 | $methodName = $method->getShortName(); 36 | if ($allowsNull) { 37 | $methodBody = <<__memoized) || !array_key_exists('$methodName', \$this->__memoized)) { 39 | \$this->__memoized['$methodName'] = parent::$methodName(); 40 | } 41 | return \$this->__memoized['$methodName']; 42 | THEPHP; 43 | } else { 44 | $methodBody = <<__memoized['$methodName'] ?? (\$this->__memoized['$methodName'] = parent::$methodName()); 46 | THEPHP; 47 | } 48 | return $methodDefinitions->withAdditionalMethodDefinition(MethodDefinition::of($method, $methodBody)); 49 | } 50 | ); 51 | } 52 | } -------------------------------------------------------------------------------- /src/BuilderClass/SetterMethodGenerator.php: -------------------------------------------------------------------------------- 1 | 14 | */ 15 | class SetterMethodGenerator implements MethodGenerator 16 | { 17 | public function generateMethods(ReflectionMethodCollection $matchedMethods, PropertyCollection $properties): MethodDefinitionCollection 18 | { 19 | return $matchedMethods 20 | ->filterAbstract() 21 | ->filter(self::abstractMethodMatcher()) 22 | ->reduce(MethodDefinitionCollection::create(), function (MethodDefinitionCollection $methodDefinitions, ReflectionMethod $method) { 23 | $propertyName = self::getPropertyName($method->getShortName()); 24 | $parameterName = $method->getParameters()[0]->getName(); 25 | $methodBody = <<propertyValues['$propertyName'] = \${$parameterName}; 27 | return \$this; 28 | THEPHP; 29 | return $methodDefinitions->withAdditionalMethodDefinition(MethodDefinition::of($method, $methodBody)); 30 | }); 31 | } 32 | 33 | private static function getPropertyName(string $accessorMethodName): string 34 | { 35 | return getPropertyName($accessorMethodName, 'set') ?? $accessorMethodName; 36 | } 37 | 38 | private static function abstractMethodMatcher(): callable 39 | { 40 | return function (ReflectionMethod $reflectionMethod) { 41 | if ($reflectionMethod->getNumberOfParameters() !== 1) { 42 | return false; 43 | } 44 | $returnType = $reflectionMethod->getReturnType(); 45 | if (!$returnType) { 46 | return true; 47 | } 48 | if ($returnType->allowsNull()) { 49 | return false; 50 | } 51 | if ($returnType->isBuiltin()) { 52 | return (string)$returnType === 'self'; 53 | } 54 | return $reflectionMethod->getDeclaringClass()->isSubclassOf((string)$returnType); 55 | }; 56 | } 57 | } -------------------------------------------------------------------------------- /src/Console/BuildCommand.php: -------------------------------------------------------------------------------- 1 | 12 | */ 13 | class BuildCommand extends Command 14 | { 15 | private $templateDirectoryProcessor; 16 | 17 | public function __construct(TemplateDirectoryProcessor $templateDirectoryProcessor) 18 | { 19 | $this->templateDirectoryProcessor = $templateDirectoryProcessor; 20 | parent::__construct('build'); 21 | } 22 | 23 | protected function configure() 24 | { 25 | $this->addArgument('source', InputArgument::REQUIRED); 26 | $this->addArgument('target', InputArgument::OPTIONAL); 27 | } 28 | 29 | protected function execute(InputInterface $input, OutputInterface $output) 30 | { 31 | $sourceDirectory = \realpath($input->getArgument('source')); 32 | if (!$sourceDirectory) { 33 | throw new \Exception('The specified source directory does not exist.'); 34 | } 35 | $specifiedTargetDirectory = $input->getArgument('target'); 36 | if (!$specifiedTargetDirectory) { 37 | $targetDirectory = $sourceDirectory; 38 | } else { 39 | $targetDirectory = \realpath($specifiedTargetDirectory); 40 | if (!$targetDirectory) { 41 | throw new \Exception('The specified target directory does not exist.'); 42 | } 43 | } 44 | 45 | foreach ($this->templateDirectoryProcessor->generateAutoClasses($sourceDirectory) as [$relativeFilePath, $autoClass]) { 46 | $absoluteFilePath = $targetDirectory . DIRECTORY_SEPARATOR . $relativeFilePath; 47 | $absoluteFilePathWithoutResolution = ($specifiedTargetDirectory ?: $sourceDirectory) . \DIRECTORY_SEPARATOR . $relativeFilePath; 48 | \fwrite(\STDOUT, "$absoluteFilePathWithoutResolution\n"); 49 | $directoryPath = \dirname($absoluteFilePath); 50 | if (!\file_exists($directoryPath)) { 51 | \mkdir($directoryPath, 0744, true); 52 | } 53 | \file_put_contents($absoluteFilePath, " 10 | */ 11 | class TemplateClassLocator 12 | { 13 | private const ANNOTATION_PATTERN = '{^\s*(/\*)?\*\s+@(?PAutoValue(\\\\[^\s\*]*)?)[\s\*]*(\*/)?$}m'; 14 | 15 | private $astLocator; 16 | 17 | public function __construct(AstLocator $astLocator) 18 | { 19 | $this->astLocator = $astLocator; 20 | } 21 | 22 | public function locateTemplateClasses(string $targetDir): \Iterator 23 | { 24 | $directoryIterator = new \RecursiveIteratorIterator(new \RecursiveDirectoryIterator($targetDir)); 25 | foreach ($directoryIterator as $path) { 26 | if (\pathinfo($path, \PATHINFO_EXTENSION) === 'php') { 27 | $relativeFilePath = $this->relativizeFilePath($targetDir, $path); 28 | foreach ($this->getTemplateClasses($path) as [$templateClassName, $annotations]) { 29 | yield TemplateClass::of($templateClassName, $relativeFilePath, $annotations); 30 | } 31 | } 32 | } 33 | } 34 | 35 | private function getTemplateClasses(string $filePath): \Iterator 36 | { 37 | $fileContents = \file_get_contents($filePath); 38 | if (\preg_match(self::ANNOTATION_PATTERN, $fileContents) !== 1) { 39 | return; 40 | } 41 | $reflector = new ClassReflector(new SingleFileSourceLocator($filePath, $this->astLocator)); 42 | foreach ($reflector->getAllClasses() as $reflectionClass) { 43 | if (\preg_match_all(self::ANNOTATION_PATTERN, $reflectionClass->getDocComment(), $matches)) { 44 | yield [$reflectionClass->getName(), $matches['annotations']]; 45 | } 46 | } 47 | } 48 | 49 | private function relativizeFilePath(string $baseDir, string $filePath): string 50 | { 51 | $realDirPath = \realpath($baseDir); 52 | $realFilePath = \realpath($filePath); 53 | $relativePathOffset = \strpos($realFilePath, $realDirPath); 54 | if ($relativePathOffset !== 0) { 55 | return $realFilePath; 56 | } 57 | return \ltrim(\substr($realFilePath, \strlen($baseDir)), \DIRECTORY_SEPARATOR); 58 | } 59 | } -------------------------------------------------------------------------------- /src/BuilderClass/BuilderClassType.php: -------------------------------------------------------------------------------- 1 | 12 | */ 13 | class BuilderClassType implements AutoClassType 14 | { 15 | private $propertyInferrer; 16 | private $methodGenerators; 17 | private $classGenerator; 18 | 19 | public function __construct( 20 | PropertyInferrer $propertyInferrer, 21 | MethodGeneratorList $methodGenerators, 22 | BuilderClassGenerator $classGenerator 23 | ) { 24 | $this->propertyInferrer = $propertyInferrer; 25 | $this->methodGenerators = $methodGenerators; 26 | $this->classGenerator = $classGenerator; 27 | } 28 | 29 | public static function withDefaultConfiguration(PropertyInferrer $propertyInferrer): self 30 | { 31 | $methodGenerators = new MethodGeneratorList([ 32 | new SetterMethodGenerator(), 33 | new BuildMethodGenerator(), 34 | ]); 35 | return new self($propertyInferrer, $methodGenerators, new BuilderClassGenerator()); 36 | } 37 | 38 | public function annotation(): string 39 | { 40 | return 'AutoValue\\Builder'; 41 | } 42 | 43 | public function generateAutoClass(ClassReflector $reflector, string $templateBuilderClassName): string 44 | { 45 | $templateBuilderClass = $reflector->reflect($templateBuilderClassName); 46 | $templateValueClassName = self::getValueClass($templateBuilderClassName); 47 | $properties = $this->propertyInferrer->inferProperties($reflector, $templateValueClassName); 48 | $methods = ReflectionMethodCollection::of($templateBuilderClass->getMethods()); 49 | $methodDefinitions = $this->methodGenerators->generateMethods($methods, $properties); 50 | return $this->classGenerator->generateClass($templateBuilderClass, $methodDefinitions); 51 | } 52 | 53 | public static function getBuilderClass(string $valueClass): string 54 | { 55 | return "{$valueClass}Builder"; 56 | } 57 | 58 | public static function getValueClass(string $builderClass): string 59 | { 60 | if (\substr($builderClass, -7) !== 'Builder') { 61 | throw new \Exception("Builder class names must end with the word 'Builder'."); 62 | } 63 | return \substr($builderClass, 0, -7); 64 | } 65 | } -------------------------------------------------------------------------------- /src/ValueToBuilder/ToBuilderMethodProcessor.php: -------------------------------------------------------------------------------- 1 | 14 | */ 15 | class ToBuilderMethodProcessor extends MethodProcessor 16 | { 17 | public function matchMethods(ReflectionMethodCollection $methods): array 18 | { 19 | return $methods 20 | ->filterAbstract() 21 | ->filter(function (ReflectionMethod $reflectionMethod) { 22 | if (!( 23 | $reflectionMethod->getName() === 'toBuilder' 24 | && $reflectionMethod->getNumberOfParameters() === 0 25 | && $reflectionMethod->hasReturnType() 26 | )) { 27 | return false; 28 | } 29 | $returnType = $reflectionMethod->getReturnType(); 30 | if ($returnType->isBuiltin()) { 31 | return false; 32 | } 33 | $valueClassName = $reflectionMethod->getDeclaringClass()->getName(); 34 | $builderClassName = BuilderClassType::getBuilderClass($valueClassName); 35 | return $returnType->targetReflectionClass()->getName() === $builderClassName; 36 | }) 37 | ->methodNames(); 38 | } 39 | 40 | public function generateMethods(ReflectionMethodCollection $matchedMethods, PropertyCollection $properties): MethodDefinitionCollection 41 | { 42 | return $matchedMethods->reduce(MethodDefinitionCollection::create(), function (MethodDefinitionCollection $methodDefinitions, ReflectionMethod $method) use ($properties) { 43 | $builderTemplateClass = $method->getReturnType()->targetReflectionClass()->getShortName(); 44 | $builderAutoClass = "AutoValue_$builderTemplateClass"; 45 | $propertyReads = \implode($properties->mapPropertyNames(function (string $propertyName) { 46 | return "\n '$propertyName' => \$this->$propertyName,"; 47 | })); 48 | $methodBody = " return $builderAutoClass::___withTrustedValues([$propertyReads 49 | ]);"; 50 | return $methodDefinitions->withAdditionalMethodDefinition(MethodDefinition::of($method, $methodBody)); 51 | }); 52 | } 53 | } -------------------------------------------------------------------------------- /src/ValueClass/ValueClassType.php: -------------------------------------------------------------------------------- 1 | 17 | */ 18 | class ValueClassType implements AutoClassType, PropertyInferrer 19 | { 20 | private $methodProcessors; 21 | private $classGenerator; 22 | 23 | public function __construct(MethodProcessorList $methodProcessors, ValueClassGenerator $classGenerator) 24 | { 25 | $this->methodProcessors = $methodProcessors; 26 | $this->classGenerator = $classGenerator; 27 | } 28 | 29 | public static function withDefaultConfiguration(): self 30 | { 31 | $methodProcessors = new MethodProcessorList([ 32 | new EqualsMethodProcessor(), 33 | new ToBuilderMethodProcessor(), 34 | new ToArrayMethodProcessor(), 35 | new WitherMethodProcessor(), 36 | new AccessorMethodProcessor(), 37 | new MemoizeMethodProcessor(), 38 | ]); 39 | $classGenerator = new ValueClassGenerator(); 40 | return new self($methodProcessors, $classGenerator); 41 | } 42 | 43 | public function annotation(): string 44 | { 45 | return 'AutoValue'; 46 | } 47 | 48 | public function inferProperties(ClassReflector $reflector, string $templateValueClasName): PropertyCollection 49 | { 50 | $templateValueClass = $reflector->reflect($templateValueClasName); 51 | $methods = ReflectionMethodCollection::of($templateValueClass->getMethods()); 52 | [$properties] = $this->methodProcessors->processMethods($methods); 53 | return $properties; 54 | } 55 | 56 | public function generateAutoClass(ClassReflector $reflector, string $templateValueClasName): string 57 | { 58 | $templateValueClass = $reflector->reflect($templateValueClasName); 59 | $methods = ReflectionMethodCollection::of($templateValueClass->getMethods()); 60 | [$properties, $methodDefinitions] = $this->methodProcessors->processMethods($methods); 61 | return $this->classGenerator->generateClass($templateValueClass, $properties, $methodDefinitions); 62 | } 63 | } -------------------------------------------------------------------------------- /src/ReflectionMethodCollection.php: -------------------------------------------------------------------------------- 1 | 8 | */ 9 | class ReflectionMethodCollection implements \IteratorAggregate 10 | { 11 | /** 12 | * @param ReflectionMethod[] $methods 13 | */ 14 | public static function of(array $methods): self 15 | { 16 | $collection = new self; 17 | foreach ($methods as $method) { 18 | $methodName = $method->getShortName(); 19 | if (isset($collection->items[$methodName])) { 20 | throw new \Exception(); 21 | } 22 | $collection->items[$methodName] = $method; 23 | } 24 | return $collection; 25 | } 26 | 27 | public function methodNames(): array 28 | { 29 | return \array_keys($this->items); 30 | } 31 | 32 | public function getMethod(string $methodName): ?ReflectionMethod 33 | { 34 | return $this->items[$methodName] ?? null; 35 | } 36 | 37 | public function withoutMethods(array $methodNames): self 38 | { 39 | return $this->filter(function (ReflectionMethod $method) use ($methodNames) { 40 | return !\in_array($method->getShortName(), $methodNames, true); 41 | }); 42 | } 43 | 44 | public function filter(callable $predicate): self 45 | { 46 | $result = new self; 47 | $result->items = \array_filter($this->items, $predicate); 48 | return $result; 49 | } 50 | 51 | public function filterAbstract(): self 52 | { 53 | return $this->filter(function (ReflectionMethod $method): bool { 54 | return \ReflectionMethod::IS_ABSTRACT & $method->getModifiers() 55 | || $method->getDeclaringClass()->isInterface(); 56 | }); 57 | } 58 | 59 | public function filterConcrete(): self 60 | { 61 | return $this->filter(function (ReflectionMethod $method): bool { 62 | return !(\ReflectionMethod::IS_ABSTRACT & $method->getModifiers()) 63 | && !$method->getDeclaringClass()->isInterface(); 64 | }); 65 | } 66 | 67 | public function reduce($value, callable $fn) 68 | { 69 | return \array_reduce($this->items, $fn, $value); 70 | } 71 | 72 | public function isEmpty(): bool 73 | { 74 | return empty($this->items); 75 | } 76 | 77 | public function getIterator(): \Iterator 78 | { 79 | return new \ArrayIterator($this->items); 80 | } 81 | 82 | private $items = []; 83 | 84 | private function __construct() 85 | { 86 | } 87 | } -------------------------------------------------------------------------------- /src/ValueClass/ValueClassGenerator.php: -------------------------------------------------------------------------------- 1 | 14 | */ 15 | class ValueClassGenerator 16 | { 17 | public function generateClass(ReflectionClass $baseClass, PropertyCollection $properties, MethodDefinitionCollection $methodDefinitions): string 18 | { 19 | // todo include type in properties constant 20 | $propertyDeclarations = \implode("\n", $properties->map(function (Property $property) use ($baseClass) { 21 | return <<docBlockType()} */ 23 | private \${$property->name()}; 24 | THEPHP; 25 | })); 26 | 27 | $methodDeclarations = \implode("\n\n", $methodDefinitions->map(function (MethodDefinition $methodDefinition) { 28 | return generateConcreteMethod($methodDefinition->reflection(), $methodDefinition->body()); 29 | })); 30 | 31 | $requiredProperties = $properties 32 | ->filter(function (Property $property) { return $property->isRequired(); }) 33 | ->propertyNames(); 34 | 35 | $requiredPropertiesExported = \implode(', ', \array_map(function ($property) { return "'$property'"; }, $requiredProperties)); 36 | 37 | return <<getNamespaceName()}; 39 | 40 | /** 41 | * @internal 42 | */ 43 | final class AutoValue_{$baseClass->getShortName()} extends {$baseClass->getShortName()} 44 | { 45 | $propertyDeclarations 46 | 47 | protected function __construct(array \$propertyValues = []) 48 | { 49 | self::___checkRequiredPropertiesExist(\$propertyValues); 50 | 51 | foreach (\$propertyValues as \$property => \$value) { 52 | \$this->\$property = \$value; 53 | } 54 | } 55 | 56 | $methodDeclarations 57 | 58 | /** 59 | * @internal 60 | */ 61 | public static function ___checkRequiredPropertiesExist(array \$propertyValues): void 62 | { 63 | foreach ([$requiredPropertiesExported] as \$property) { 64 | if (!isset(\$propertyValues[\$property])) { 65 | throw new \Exception("Required property \$property not initialized."); 66 | } 67 | } 68 | } 69 | 70 | /** 71 | * @internal 72 | */ 73 | public static function ___withTrustedValues(array \$propertyValues): self 74 | { 75 | return new self(\$propertyValues); 76 | } 77 | } 78 | THEPHP; 79 | } 80 | } -------------------------------------------------------------------------------- /docs/practices.md: -------------------------------------------------------------------------------- 1 | # Best practices 2 | 3 | ## "Equals means interchangeable" 4 | 5 | Don't use AutoValue to implement value semantics unless you really want value 6 | semantics. In particular, you should never care about the difference between two 7 | equal instances. 8 | 9 | ## Avoid mutable property types 10 | 11 | Avoid mutable types for your properties, especially if you make your accessor 12 | methods `public`. The generated accessors don't copy the field value on its way 13 | out, so you'd be exposing your internal state. 14 | 15 | Note that this doesn't mean your factory method can't *accept* mutable types as 16 | input parameters. Example: 17 | 18 | ```php 19 | /** 20 | * @AutoValue 21 | */ 22 | abstract class DateExample 23 | { 24 | abstract function date(): ImmutableDateTime; 25 | 26 | static function create(DateTime $mutableDateTime): self 27 | { 28 | return new AutoValue_DateExample(['date' => ImmutableDateTime::fromMutable($mutableDateTime)]); 29 | } 30 | } 31 | ``` 32 | 33 | ## Keep behavior simple and dependency-free 34 | 35 | Your class can (and should) contain *simple* intrinsic behavior. But it 36 | shouldn't require complex dependencies and shouldn't access static state. 37 | 38 | You should essentially *never* need an alternative implementation of your 39 | hand-written abstract class, whether hand-written or generated by a mocking 40 | framework. If your behavior has enough complexity (or dependencies) that it 41 | actually needs to be mocked or faked, split it into a separate type that is 42 | *not* a value type. Otherwise it permits an instance with "real" behavior and 43 | one with "mock/fake" behavior to be `equals`, which does not make sense. 44 | 45 | ## One reference only 46 | 47 | Other code in the same package will be able to directly access the generated 48 | class, but *should not*. It's best if each generated class has one and only one 49 | reference from your source code: the call from your static factory method to the 50 | generated constructor. If you have multiple factory methods, have them all 51 | delegate to the same hand-written method, so there is still only one point of 52 | contact with the generated code. This way, you have only one place to insert 53 | precondition checks or other pre- or postprocessing. 54 | 55 | ## Mark all concrete methods `final` 56 | 57 | Consider that other developers will try to read and understand your value class 58 | while looking only at your hand-written class, not the actual (generated) 59 | implementation class. If you mark your concrete methods `final`, they won't have 60 | to wonder whether the generated subclass might be overriding them. This is 61 | especially helpful if you are *[underriding](howto.md#custom)* `equals`! -------------------------------------------------------------------------------- /src/ValueClass/MethodProcessorList.php: -------------------------------------------------------------------------------- 1 | 11 | */ 12 | class MethodProcessorList 13 | { 14 | private $methodProcessors; 15 | 16 | /** 17 | * @param MethodProcessor $methodProcessors 18 | */ 19 | public function __construct(array $methodProcessors) 20 | { 21 | $this->methodProcessors = $methodProcessors; 22 | } 23 | 24 | public function processMethods(ReflectionMethodCollection $methods): array 25 | { 26 | $unprocessedMethods = $methods; 27 | $matchedMethodsByProcessor = []; 28 | /** @var MethodProcessor $methodProcessor */ 29 | foreach ($this->methodProcessors as $methodProcessor) { 30 | $matchedMethodNames = $methodProcessor->matchMethods($unprocessedMethods); 31 | $matchedMethods = $unprocessedMethods->filter(function (ReflectionMethod $reflectionMethod) use ($matchedMethodNames) { 32 | return \in_array($reflectionMethod->getShortName(), $matchedMethodNames, true); 33 | }); 34 | $unprocessedMethods = $unprocessedMethods->withoutMethods($matchedMethodNames); 35 | $matchedMethodsByProcessor[] = [$methodProcessor, $matchedMethods]; 36 | } 37 | if (!$unprocessedMethods->filterAbstract()->isEmpty()) { 38 | throw new \Exception('Some abstract methods could not be processed.'); 39 | } 40 | $properties = $this->inferProperties($matchedMethodsByProcessor); 41 | $methodDefinitions = $this->generateMethods($matchedMethodsByProcessor, $properties); 42 | return [$properties, $methodDefinitions]; 43 | } 44 | 45 | private function inferProperties(array $matchedMethodsByProcessor): PropertyCollection 46 | { 47 | $properties = PropertyCollection::create(); 48 | /** 49 | * @var MethodProcessor $methodProcessor 50 | * @var ReflectionMethodCollection $matchedMethods 51 | */ 52 | foreach ($matchedMethodsByProcessor as [$methodProcessor, $matchedMethods]) { 53 | $properties = $properties->plus($methodProcessor->inferProperties($matchedMethods)); 54 | } 55 | return $properties; 56 | } 57 | 58 | private function generateMethods(array $matchedMethodsByProcessor, PropertyCollection $properties): MethodDefinitionCollection 59 | { 60 | $methodDefinitions = MethodDefinitionCollection::create(); 61 | /** 62 | * @var MethodProcessor $methodProcessor 63 | * @var ReflectionMethodCollection $matchedMethods 64 | */ 65 | foreach ($matchedMethodsByProcessor as [$methodProcessor, $matchedMethods]) { 66 | $methodDefinitions = $methodDefinitions->plus($methodProcessor->generateMethods($matchedMethods, $properties)); 67 | } 68 | return $methodDefinitions; 69 | } 70 | } -------------------------------------------------------------------------------- /docs/builders.md: -------------------------------------------------------------------------------- 1 | # AutoValue with Builders 2 | 3 | The [introduction](index.md) of this User Guide covers the basic usage of 4 | AutoValue using a static factory method as your public creation API. But in many 5 | circumstances (such as those laid out in *Effective Java, 2nd Edition* Item 2), 6 | you may prefer to let your callers use a *builder* instead. 7 | 8 | Fortunately, AutoValue can generate builder classes too! This page explains how. 9 | Note that we recommend reading and understanding the basic usage shown in the 10 | [introduction](index.md) first. 11 | 12 | ## How to use AutoValue with Builders 13 | 14 | As explained in the introduction, the AutoValue concept is that **you write an 15 | abstract value class, and AutoValue implements it**. Builder generation works in 16 | the exact same way: you also create an abstract builder class, nesting it inside 17 | your abstract value class, and AutoValue generates implementations for both. 18 | 19 | ### In `Animal.php` 20 | 21 | ```php 22 | /** 23 | * @AutoValue 24 | */ 25 | abstract class Animal 26 | { 27 | abstract function name(): string; 28 | abstract function numberOfLegs(): int; 29 | 30 | static function builder(): AnimalBuilder 31 | { 32 | return new AutoValue_AnimalBuilder(); 33 | } 34 | } 35 | ``` 36 | 37 | ### In `AnimalBuilder.php` 38 | 39 | ```php 40 | /** 41 | * @AutoValue\Builder 42 | */ 43 | abstract class AnimalBuilder 44 | { 45 | abstract function name(string $value): self; 46 | abstract function numberOfLegs(int $value): self; 47 | abstract function build(): Animal; 48 | } 49 | ``` 50 | 51 | Note that in real life, some classes and methods would presumably have PHPDoc. 52 | We're leaving these off in the User Guide only to keep the examples clean and 53 | short. 54 | 55 | ### Usage 56 | 57 | ```php 58 | public function testAnimal() 59 | { 60 | $dog = Animal::builder()->name("dog")->numberOfLegs(4)->build(); 61 | self::assertEquals("dog", $dog->name()); 62 | self::assertEquals(4, $dog->numberOfLegs()); 63 | 64 | // You probably don't need to write assertions like these; just illustrating. 65 | self::assertTrue( 66 | Animal::builder()->name("dog")->numberOfLegs(4)->build()->equals($dog)); 67 | self::assertFalse( 68 | Animal::builder()->name("dog")->numberOfLegs(4)->build()->equals($dog)); 69 | self::assertFalse( 70 | Animal::builder()->name("dog")->numberOfLegs(2)->build()->equals($dog)); 71 | } 72 | ``` 73 | 74 | ### What does AutoValue generate? 75 | 76 | For the `Animal` example shown above, here is [typical code AutoValue might 77 | generate](generated-builder-example.md). 78 | 79 | ## How do I... 80 | 81 | * ... [use (or not use) `set` **prefixes**?](builders-howto.md#beans) 82 | * ... [use different **names** besides 83 | `builder()`/`Builder`/`build()`?](builders-howto.md#build_names) 84 | * ... [specify a **default** value for a property?](builders-howto.md#default) 85 | * ... [initialize a builder to the same property values as an **existing** 86 | value instance](builders-howto.md#to_builder) 87 | * ... [**validate** property values?](builders-howto.md#validate) 88 | -------------------------------------------------------------------------------- /src/ValueClass/AccessorMethodProcessor.php: -------------------------------------------------------------------------------- 1 | 14 | */ 15 | class AccessorMethodProcessor extends MethodProcessor 16 | { 17 | public function matchMethods(ReflectionMethodCollection $methods): array 18 | { 19 | return $methods 20 | ->filterAbstract() 21 | ->filter(function (ReflectionMethod $reflectionMethod) { 22 | return $reflectionMethod->getNumberOfParameters() === 0; 23 | }) 24 | ->methodNames(); 25 | } 26 | 27 | public function inferProperties(ReflectionMethodCollection $matchedMethods): PropertyCollection 28 | { 29 | $templateUsesPrefixes = self::templateUsesPrefixes($matchedMethods); 30 | return $matchedMethods->reduce(PropertyCollection::create(), function (PropertyCollection $properties, ReflectionMethod $method) use ($templateUsesPrefixes) { 31 | $propertyName = self::getPropertyName($templateUsesPrefixes, $method); 32 | $property = Property::fromAccessorMethod($propertyName, $method); 33 | return $properties->withProperty($property); 34 | }); 35 | } 36 | 37 | public function generateMethods(ReflectionMethodCollection $matchedMethods, PropertyCollection $properties): MethodDefinitionCollection 38 | { 39 | $templateUsesPrefixes = self::templateUsesPrefixes($matchedMethods); 40 | return $matchedMethods->reduce(MethodDefinitionCollection::create(), function (MethodDefinitionCollection $methodDefinitions, ReflectionMethod $method) use ($templateUsesPrefixes, $properties) { 41 | $propertyName = self::getPropertyName($templateUsesPrefixes, $method); 42 | $methodBody = " return \$this->$propertyName;"; 43 | return $methodDefinitions->withAdditionalMethodDefinition(MethodDefinition::of($method, $methodBody)); 44 | }); 45 | } 46 | 47 | private static function getPropertyName(bool $templateUsesPrefixes, ReflectionMethod $matchedMethod): ?string 48 | { 49 | $methodName = $matchedMethod->getShortName(); 50 | if (!$templateUsesPrefixes) { 51 | return $methodName; 52 | } 53 | if (($type = $matchedMethod->getReturnType()) && (string)$type === 'bool') { 54 | return (getPropertyName($methodName, 'get') ?? getPropertyName($methodName, 'is')); 55 | } 56 | return getPropertyName($methodName, 'get'); 57 | } 58 | 59 | private static function templateUsesPrefixes(ReflectionMethodCollection $matchedMethods): bool 60 | { 61 | /** @var ReflectionMethod $matchedMethod */ 62 | foreach ($matchedMethods as $matchedMethod) { 63 | $propertyName = self::getPropertyName(true, $matchedMethod); 64 | if ($propertyName === null) { 65 | return false; 66 | } 67 | } 68 | return true; 69 | } 70 | } -------------------------------------------------------------------------------- /src/functions.php: -------------------------------------------------------------------------------- 1 | isProtected() ? 'protected' : 'public'; 23 | $parameters = generateParameters($abstractMethod->getParameters()); 24 | $returnTypeHint = generateReturnTypeHint($abstractMethod->getReturnType(), $abstractMethod->getDeclaringClass()); 25 | return "$visibility function {$abstractMethod->getShortName()}({$parameters}){$returnTypeHint}"; 26 | } 27 | 28 | function generateParameters(array $reflectionParameters): string 29 | { 30 | return \implode(', ', \array_map(function (ReflectionParameter $parameter) { 31 | $typeHintPart = $parameter->getType() ? generateTypeHint($parameter->getType(), $parameter->getDeclaringClass()) . ' ' : ''; 32 | $variadicPart = $parameter->isVariadic() ? '...' : ''; 33 | $defaultValuePart = $parameter->isDefaultValueAvailable() ? ' = ' . \var_export($parameter->getDefaultValue(), true) : ''; 34 | return $typeHintPart . $variadicPart . '$' . $parameter->getName() . $defaultValuePart; 35 | }, $reflectionParameters)); 36 | } 37 | 38 | function generateReturnTypeHint(?ReflectionType $returnType, ReflectionClass $declaringClass): string 39 | { 40 | return $returnType === null ? '' : ': ' . generateTypeHint($returnType, $declaringClass); 41 | } 42 | 43 | function generateTypeHint(ReflectionType $type, ReflectionClass $declaringClass): string 44 | { 45 | $prefix = $type->allowsNull() ? '?' : ''; 46 | $normalizedType = (string)$type === 'self' 47 | ? '\\' . $declaringClass->getName() 48 | : ($type->isBuiltin() ? (string)$type : '\\' . (string)$type); 49 | return $prefix . $normalizedType; 50 | } 51 | 52 | function getPropertyName(string $methodName, string $prefix): ?string 53 | { 54 | if (\strpos($methodName, $prefix) !== 0) { 55 | return null; 56 | } 57 | 58 | $propertyName = \substr($methodName, \strlen($prefix)); 59 | 60 | return \strlen($propertyName) > 0 && \ucfirst($propertyName) === $propertyName 61 | ? \lcfirst($propertyName) 62 | : null; 63 | } 64 | 65 | function splitFullyQualifiedName(string $fullyQualifiedName): array 66 | { 67 | $lastSlashOffset = \strrpos($fullyQualifiedName, '\\'); 68 | if ($lastSlashOffset === false) { 69 | return ['', $fullyQualifiedName]; 70 | } 71 | return [ 72 | \substr($fullyQualifiedName, 0, $lastSlashOffset), 73 | \substr($fullyQualifiedName, $lastSlashOffset + 1), 74 | ]; 75 | } 76 | 77 | function isClass(ReflectionType $reflectionType): bool 78 | { 79 | return (string)$reflectionType === 'self' || !$reflectionType->isBuiltin(); 80 | } 81 | 82 | function getClass(ReflectionClass $declaringClass, ReflectionType $reflectionType): ?ReflectionClass 83 | { 84 | if ((string)$reflectionType === 'self') { 85 | return $declaringClass; 86 | } 87 | if (!$reflectionType->isBuiltin()) { 88 | return $reflectionType->targetReflectionClass(); 89 | } 90 | return null; 91 | } -------------------------------------------------------------------------------- /demo/auto/AutoValue_Address.php: -------------------------------------------------------------------------------- 1 | $value) { 25 | $this->$property = $value; 26 | } 27 | } 28 | 29 | public function equals($foo): bool 30 | { 31 | $typedPropertiesAreEqual = $foo instanceof self 32 | && $this->city === $foo->city 33 | && $this->country === $foo->country 34 | && $this->postCode->equals($foo->postCode); 35 | if (!$typedPropertiesAreEqual) { 36 | return false; 37 | } 38 | $compareValues = static function ($value1, $value2) use (&$compareValues) { 39 | if (\is_array($value1)) { 40 | $equal = \is_array($value2) && \count($value1) === \count($value2) && !\array_udiff_assoc($value1, $value2, $compareValues); 41 | } else { 42 | $equal = $value1 === $value2 || (\method_exists($value1, 'equals') ? $value1->equals($value2) : \is_object($value1) && $value1 == $value2); 43 | } 44 | return $equal ? 0 : 1; 45 | }; 46 | return $compareValues($this->metadata, $foo->metadata) === 0 47 | && $compareValues($this->foo, $foo->foo) === 0 48 | && \count($this->lines) === \count($foo->lines) && !\array_udiff_assoc($this->lines, $foo->lines, $compareValues); 49 | } 50 | 51 | public function toBuilder(): \AutoValue\Demo\AddressBuilder 52 | { 53 | return AutoValue_AddressBuilder::___withTrustedValues([ 54 | 'lines' => $this->lines, 55 | 'city' => $this->city, 56 | 'country' => $this->country, 57 | 'postCode' => $this->postCode, 58 | 'metadata' => $this->metadata, 59 | 'foo' => $this->foo, 60 | ]); 61 | } 62 | 63 | public function withLines(string ...$lines): \AutoValue\Demo\Address 64 | { 65 | $result = clone $this; 66 | unset($result->__memoized); 67 | $result->lines = $lines; 68 | return $result; 69 | } 70 | 71 | public function withCountry(string $country): \AutoValue\Demo\Address 72 | { 73 | $result = clone $this; 74 | unset($result->__memoized); 75 | $result->country = $country; 76 | return $result; 77 | } 78 | 79 | public function lines(): array 80 | { 81 | return $this->lines; 82 | } 83 | 84 | public function city(): ?string 85 | { 86 | return $this->city; 87 | } 88 | 89 | public function country(): string 90 | { 91 | return $this->country; 92 | } 93 | 94 | public function postCode(): \AutoValue\Demo\PostCode 95 | { 96 | return $this->postCode; 97 | } 98 | 99 | public function metadata() 100 | { 101 | return $this->metadata; 102 | } 103 | 104 | public function foo() 105 | { 106 | return $this->foo; 107 | } 108 | 109 | public function linesString(): string 110 | { 111 | return $this->__memoized['linesString'] ?? ($this->__memoized['linesString'] = parent::linesString()); 112 | } 113 | 114 | /** 115 | * @internal 116 | */ 117 | public static function ___withTrustedValues(array $propertyValues): self 118 | { 119 | return new self($propertyValues); 120 | } 121 | } 122 | -------------------------------------------------------------------------------- /src/TemplateDirectoryProcessor.php: -------------------------------------------------------------------------------- 1 | 13 | */ 14 | class TemplateDirectoryProcessor 15 | { 16 | private $astLocator; 17 | private $autoClassLocator; 18 | private $autoClassTypeMap; 19 | private $sourceLocator; 20 | 21 | /** 22 | * @param AutoClassType[] $autoClassTypes 23 | */ 24 | public function __construct( 25 | AstLocator $astLocator, 26 | TemplateClassLocator $templateClassLocator, 27 | array $autoClassTypes, 28 | SourceLocator $sourceLocator 29 | ) { 30 | $this->astLocator = $astLocator; 31 | $this->autoClassLocator = $templateClassLocator; 32 | $this->autoClassTypeMap = []; 33 | foreach ($autoClassTypes as $autoClassType) { 34 | $this->autoClassTypeMap[$autoClassType->annotation()] = $autoClassType; 35 | } 36 | $this->sourceLocator = $sourceLocator; 37 | } 38 | 39 | public function generateAutoClasses(string $directory): \Iterator 40 | { 41 | $composerClassLoader = $this->getComposerClassLoader($directory); 42 | $composerSourceLocator = new ComposerSourceLocator($composerClassLoader, $this->astLocator); 43 | $sourceLocator = new AggregateSourceLocator([$this->sourceLocator, $composerSourceLocator]); 44 | $classReflector = new ClassReflector($sourceLocator); 45 | $templateClasses = $this->autoClassLocator->locateTemplateClasses($directory); 46 | /** @var TemplateClass $templateClass */ 47 | foreach ($templateClasses as $templateClass) { 48 | $autoClassType = $this->getClassType($templateClass); 49 | $autoFilename = 'AutoValue_' . \basename($templateClass->relativeFilePath()); 50 | $autoFilePath = \strpos($templateClass->relativeFilePath(), \DIRECTORY_SEPARATOR) !== false 51 | ? \dirname($templateClass->relativeFilePath()) . \DIRECTORY_SEPARATOR . $autoFilename 52 | : $autoFilename; 53 | yield [$autoFilePath, $autoClassType->generateAutoClass($classReflector, $templateClass->className())]; 54 | } 55 | } 56 | 57 | private function getComposerClassLoader(string $dir): ClassLoader 58 | { 59 | do { 60 | $autoloadPath = $dir . \DIRECTORY_SEPARATOR . 'vendor' . \DIRECTORY_SEPARATOR . 'autoload.php'; 61 | if (\file_exists($autoloadPath)) { 62 | return require $autoloadPath; 63 | } 64 | $previousDir = $dir; 65 | $dir = \dirname($dir); 66 | } while ($dir !== $previousDir); 67 | 68 | throw new \Exception('Failed to find vendor autoload file.'); 69 | } 70 | 71 | private function getClassType(TemplateClass $templateClass): ?AutoClassType 72 | { 73 | $matches = []; 74 | 75 | foreach ($templateClass->annotations() as $annotation) { 76 | if (isset($this->autoClassTypeMap[$annotation])) { 77 | $matches[] = $this->autoClassTypeMap[$annotation]; 78 | } 79 | } 80 | 81 | switch (\count($matches)) { 82 | case 1: 83 | return $matches[0]; 84 | 85 | case 0: 86 | throw new \Exception('No recognised AutoValue annotations found on class ' . $templateClass->className()); 87 | 88 | default: 89 | throw new \Exception('Multiple AutoValue annotations found on class ' . $templateClass->className()); 90 | } 91 | } 92 | } 93 | -------------------------------------------------------------------------------- /docs/index.md: -------------------------------------------------------------------------------- 1 | # AutoValue PHP 2 | 3 | ## How to use AutoValue 4 | 5 | The AutoValue concept is extremely simple: **You write an abstract class, and 6 | AutoValue implements it.** 7 | 8 | **Note:** Below, we will illustrate an AutoValue class *without* a generated 9 | builder class. If you're more interested in the builder support, continue 10 | reading at [AutoValue with Builders](builders.md) instead. 11 | 12 | ### In your value class 13 | 14 | Create your value class as an *abstract* class, with an abstract accessor method 15 | for each desired property, an equals method, and bearing the `@AutoValue` annotation. 16 | 17 | ```php 18 | /** 19 | * @AutoValue 20 | */ 21 | abstract class Animal 22 | { 23 | static function create(string $name, int $numberOfLegs): self 24 | { 25 | return new AutoValue_Animal([ 26 | 'name' => $name, 27 | 'numberOfLegs' => $numberOfLegs, 28 | ]); 29 | } 30 | 31 | abstract function name(): string; 32 | abstract function numberOfLegs(): int; 33 | abstract function equals($value): bool; 34 | } 35 | ``` 36 | 37 | Note that in real life, some classes and methods would presumably have PHPDoc. 38 | We're leaving these off in the User Guide only to keep the examples clean and 39 | short. 40 | 41 | ### Installation 42 | 43 | Install AutoValue in your project using [Composer](https://getcomposer.org). 44 | 45 | ```bash 46 | composer require space48/auto-value --dev 47 | ``` 48 | 49 | Note that AutoValue should be installed as a dev dependency as it does not need 50 | to be loaded in your project at runtime. 51 | 52 | ### Usage 53 | 54 | Your choice to use AutoValue is essentially *API-invisible*. That means that to 55 | the consumer of your class, your class looks and functions like any other. The 56 | simple test below illustrates that behavior. Note that in real life, you would 57 | write tests that actually *do something interesting* with the object, instead of 58 | only checking field values going in and out. 59 | 60 | ```php 61 | public function testAnimal() { 62 | $dog = Animal::create('dog', 4); 63 | self::assertEquals('dog', $dog->name()); 64 | self::assertEquals(4, $dog->numberOfLegs()); 65 | 66 | // You probably don't need to write assertions like these; just illustrating. 67 | self::assertTrue(Animal::create('dog', 4)->equals($dog)); 68 | self::assertFalse(Animal::create('cat', 4)->equals($dog)); 69 | self::assertFalse(Animal::create('dog', 2)->equals($dog)); 70 | self::assertFalse(Animal::create('dog', 2)->equals('banana')); 71 | } 72 | ``` 73 | 74 | ### Building the AutoValue classes 75 | 76 | When you create a new class bearing the `@AutoValue` annotation, or modify or 77 | remove such a class, you must run AutoValue's build command in order to rebuild 78 | the AutoValue classes. 79 | 80 | ```bash 81 | vendor/bin/auto build path/to/project/src 82 | ``` 83 | 84 | ### What's going on here? 85 | 86 | AutoValue searches the specified source directory for any abstract PHP classes 87 | bearing the `@AutoValue` annotation. It reads your abstract class and infers 88 | what the implementation class should look like. It generates source code, in 89 | your package, of a concrete implementation class which extends your abstract 90 | class, having: 91 | 92 | * one property for each of your abstract accessor methods 93 | * a constructor that sets these fields 94 | * a concrete implementation of each accessor method returning the associated 95 | property value 96 | * an `equals` implementation that compares these values in the usual way 97 | 98 | Your hand-written code, as shown above, delegates its factory method call to the 99 | generated constructor and voilà! 100 | 101 | For the `Animal` example shown above, here is [typical code AutoValue might 102 | generate](generated-example.md). 103 | 104 | Note that *consumers* of your value class *don't need to know any of this*. They 105 | just invoke your provided factory method and get a well-behaved instance back. 106 | 107 | ## Why should I use AutoValue? 108 | 109 | See [Why AutoValue?](why.md). 110 | 111 | ## How do I... 112 | 113 | How do I... 114 | 115 | * ... [also generate a **builder** for my value class?](howto.md#builder) 116 | * ... [include `with-` methods on my value class for creating slightly 117 | **altered** instances?](howto.md#withers) 118 | * ... [use (or not use) JavaBeans-style name **prefixes**?](howto.md#beans) 119 | * ... [use **nullable** properties?](howto.md#nullable) 120 | * ... [perform other **validation**?](howto.md#validate) 121 | * ... [use a property of a **mutable** type?](howto.md#mutable_property) 122 | * ... [use a **custom** implementation of `equals`, etc.?](howto.md#custom) 123 | * ... [have multiple **`create`** methods, or name it/them 124 | differently?](howto.md#create) 125 | * ... [**ignore** certain properties in `equals`, etc.?](howto.md#ignore) 126 | * ... [have AutoValue also implement abstract methods from my 127 | **supertypes**?](howto.md#supertypes) 128 | * ... [also include **setter** (mutator) methods?](howto.md#setters) 129 | * ... [have one `@AutoValue` class **extend** another?](howto.md#inherit) 130 | * ... [keep my accessor methods **private**?](howto.md#private_accessors) 131 | * ... [expose a **constructor**, not factory method, as my public creation 132 | API?](howto.md#public_constructor) 133 | * ... [use AutoValue on an **interface**, not abstract class?](howto.md#interface) 134 | * ... [**memoize** ("cache") derived properties?](howto.md#memoize) 135 | -------------------------------------------------------------------------------- /tests/Console/Build/BuildTest.php: -------------------------------------------------------------------------------- 1 | createTempDir(); 14 | $expectedAutoClassFilePaths = $this->getExpectedAutoClassFilePaths($targetDir); 15 | 16 | $autoBinPath = dirname(__DIR__, 3) . DIRECTORY_SEPARATOR . 'bin' . DIRECTORY_SEPARATOR . 'auto'; 17 | $sourceDir = __DIR__ . DIRECTORY_SEPARATOR . 'templates'; 18 | $shellCommand = sprintf( 19 | '%s %s build %s %s', 20 | \PHP_BINARY, 21 | escapeshellarg($autoBinPath), 22 | escapeshellarg($sourceDir), 23 | escapeshellarg($targetDir) 24 | ); 25 | exec($shellCommand, $stdoutLines, $exitCode); 26 | 27 | self::assertSame(0, $exitCode); 28 | self::assertArrayValuesSame($expectedAutoClassFilePaths, $stdoutLines); 29 | 30 | include $sourceDir . DIRECTORY_SEPARATOR . 'vendor' . DIRECTORY_SEPARATOR . 'autoload.php'; 31 | $this->requireFiles($expectedAutoClassFilePaths); 32 | } 33 | 34 | public function testArrayComparison(): void 35 | { 36 | $address1 = Address::builder() 37 | ->setLines('line1') 38 | ->setCountry('UK') 39 | ->setPostCode(new PostCode()) 40 | ->build(); 41 | 42 | $address2 = $address1->withLines('line1', 'line2'); 43 | 44 | self::assertFalse($address1->equals($address2)); 45 | self::assertFalse($address2->equals($address1)); 46 | } 47 | 48 | public function testAddressAutoClass(): void 49 | { 50 | $address = null; 51 | try { 52 | $address = Address::builder() 53 | ->setLines('line1') 54 | ->setCountry('UK') 55 | ->build(); 56 | } catch (\Exception $e) { 57 | self::assertSame('Required property postCode not initialized.', $e->getMessage()); 58 | } 59 | self::assertNull($address); 60 | 61 | $addressBuilder = Address::builder() 62 | ->setLines('line1') 63 | ->setCountry('UK') 64 | ->setPostCode($postCode = new PostCode()); 65 | $address1 = $addressBuilder->build(); 66 | self::assertTrue($address1->equals($address1)); 67 | self::assertTrue($address1->equals($addressBuilder->build())); 68 | self::assertSame(['line1'], $address1->lines()); 69 | self::assertSame('UK', $address1->country()); 70 | self::assertSame($postCode, $address1->postCode()); 71 | 72 | // test @Memoize 73 | self::assertSame(0, $address1->n()); 74 | self::assertSame(0, $address1->n()); 75 | self::assertTrue($address1->equals($addressBuilder->build())); 76 | 77 | $address2 = Address::builder() 78 | ->setLines('line2') 79 | ->setCountry('UK') 80 | ->setPostCode($postCode = new PostCode()) 81 | ->build(); 82 | self::assertFalse($address2->equals($address1)); 83 | } 84 | 85 | public function testCommandAutoClass(): void 86 | { 87 | $command1 = Command::of( 88 | 'SaveAddress', 89 | [ 90 | 'address' => Address::builder() 91 | ->setLines('line1') 92 | ->setCountry('UK') 93 | ->setPostCode($postCode = new PostCode()) 94 | ->build(), 95 | ] 96 | ); 97 | 98 | $command2 = $command1->withPayload([ 99 | 'address' => Address::builder() 100 | ->setLines('line2') 101 | ->setCountry('UK') 102 | ->setPostCode($postCode = new PostCode()) 103 | ->build(), 104 | ]); 105 | 106 | self::assertFalse($command1->equals($command2)); 107 | 108 | self::assertTrue($command1->equals($command2->withPayload($command1->payload()))); 109 | } 110 | 111 | public function testMemoizedValuesResetOnClone(): void 112 | { 113 | $address = Address::builder() 114 | ->setLines('foo') 115 | ->setCountry('UK') 116 | ->setPostCode(new PostCode()) 117 | ->build(); 118 | self::assertSame('foo', $address->firstLine()); 119 | $address2 = $address->withLines('bar'); 120 | self::assertSame('bar', $address2->firstLine()); 121 | } 122 | 123 | private function createTempDir(): string 124 | { 125 | $path = sys_get_temp_dir() . DIRECTORY_SEPARATOR . 'auto-value-php-tests-' . \random_int(1, 1000000); 126 | mkdir($path); 127 | return $path; 128 | } 129 | 130 | private function getExpectedAutoClassFilePaths(string $targetDir): array 131 | { 132 | return array_map(function (string $templateClassName) use ($targetDir) { 133 | return $this->getExpectedAutoClassFilePath($targetDir, $templateClassName); 134 | }, ['Address', 'AddressBuilder', 'Command']); 135 | } 136 | 137 | private function getExpectedAutoClassFilePath(string $targetDir, string $templateClassName): string 138 | { 139 | return $targetDir . DIRECTORY_SEPARATOR . "AutoValue_{$templateClassName}.php"; 140 | } 141 | 142 | private function requireFiles(array $files): void 143 | { 144 | foreach ($files as $file) { 145 | require $file; 146 | } 147 | } 148 | 149 | private static function assertArrayValuesSame($expected, $actual): void 150 | { 151 | $expectedValues = array_values($expected); 152 | $actualValues = array_values($actual); 153 | sort($expectedValues); 154 | sort($actualValues); 155 | self::assertSame($expectedValues, $actualValues); 156 | } 157 | } -------------------------------------------------------------------------------- /src/ValueEquals/EqualsMethodProcessor.php: -------------------------------------------------------------------------------- 1 | 17 | */ 18 | class EqualsMethodProcessor extends MethodProcessor 19 | { 20 | public function matchMethods(ReflectionMethodCollection $methods): array 21 | { 22 | return $methods 23 | ->filterAbstract() 24 | ->filter(\Closure::fromCallable([$this, 'matchMethod'])) 25 | ->methodNames(); 26 | } 27 | 28 | public function generateMethods(ReflectionMethodCollection $matchedMethods, PropertyCollection $properties): MethodDefinitionCollection 29 | { 30 | return $matchedMethods->reduce(MethodDefinitionCollection::create(), function (MethodDefinitionCollection $methodDefinitions, ReflectionMethod $method) use ($properties) { 31 | $valueParam = $method->getParameters()[0]->getName(); 32 | $methodBody = $this->generateMethodBody($method->getDeclaringClass(), $valueParam, $properties); 33 | $methodDefinition = MethodDefinition::of($method, $methodBody); 34 | return $methodDefinitions->withAdditionalMethodDefinition($methodDefinition); 35 | }); 36 | } 37 | 38 | private function matchMethod(ReflectionMethod $reflectionMethod): bool 39 | { 40 | return $reflectionMethod->getNumberOfParameters() === 1 41 | && $reflectionMethod->getNumberOfRequiredParameters() === 1 42 | && !$reflectionMethod->getParameters()[0]->hasType() 43 | && $reflectionMethod->hasReturnType() 44 | && ($returnType = $reflectionMethod->getReturnType()) 45 | && (string)$returnType === 'bool'; 46 | } 47 | 48 | private function generateMethodBody( 49 | ReflectionClass $templateClass, 50 | string $valueParam, 51 | PropertyCollection $properties 52 | ): string { 53 | $typedProperties = $properties->filter(function (Property $property) { return $property->phpType() !== null; }); 54 | $arrayProperties = $typedProperties->filter(function (Property $property) { return (string)$property->phpType() === 'array'; }); 55 | $classProperties = $typedProperties->filter(function (Property $property) { return isClass($property->phpType()); }); 56 | $valueObjectProperties = $classProperties->filter(function (Property $property) use ($templateClass) { 57 | return $this->isValueObject($templateClass, $property); 58 | }); 59 | $mixedProperties = $properties->filter(function (Property $property) { 60 | return $property->phpType() === null 61 | || (string)$property->phpType() === 'object' 62 | || (string)$property->phpType() === 'iterable' 63 | || (string)$property->phpType() === 'callable'; 64 | }); 65 | 66 | $testsForTypedProperties = \array_merge( 67 | ["\${$valueParam} instanceof self"], 68 | 69 | $typedProperties 70 | ->filter(function (Property $property) { return !isClass($property->phpType()); }) 71 | ->filter(function (Property $property) { return (string)$property->phpType() !== 'array'; }) 72 | ->mapPropertyNames(function (string $propertyName) use ($valueParam) { 73 | return "\$this->$propertyName === \${$valueParam}->$propertyName"; 74 | }), 75 | 76 | $valueObjectProperties 77 | ->filter(function (Property $property) { return $property->isRequired(); }) 78 | ->mapPropertyNames(function (string $propertyName) use ($valueParam) { 79 | return "\$this->{$propertyName}->equals(\${$valueParam}->$propertyName)"; 80 | }), 81 | 82 | $valueObjectProperties 83 | ->filter(function (Property $property) { return !$property->isRequired(); }) 84 | ->mapPropertyNames(function (string $propertyName) use ($valueParam) { 85 | return "(\$this->{$propertyName} === null ? \${$valueParam}->$propertyName === null : \$this->{$propertyName}->equals(\${$valueParam}->$propertyName))"; 86 | }), 87 | 88 | $classProperties 89 | ->filter(function (Property $property) use ($templateClass) { return !$this->isValueObject($templateClass, $property); }) 90 | ->mapPropertyNames(function (string $propertyName) use ($valueParam) { 91 | return "\$this->$propertyName == \${$valueParam}->$propertyName"; 92 | }) 93 | ); 94 | 95 | $testsForMixedProperties = \array_merge( 96 | $mixedProperties 97 | ->mapPropertyNames(function (string $propertyName) use ($valueParam) { 98 | return "\$compareValues(\$this->{$propertyName}, \${$valueParam}->$propertyName) === 0"; 99 | }), 100 | 101 | $arrayProperties 102 | ->filter(function (Property $property) { return $property->isRequired(); }) 103 | ->mapPropertyNames(function (string $propertyName) use ($valueParam) { 104 | return "\count(\$this->{$propertyName}) === \count(\${$valueParam}->$propertyName) && !\array_udiff_assoc(\$this->{$propertyName}, \${$valueParam}->$propertyName, \$compareValues)"; 105 | }), 106 | 107 | $arrayProperties 108 | ->filter(function (Property $property) { return !$property->isRequired(); }) 109 | ->mapPropertyNames(function (string $propertyName) use ($valueParam) { 110 | return "\$this->{$propertyName} === null ? \${$valueParam}->$propertyName === null : (\count(\$this->{$propertyName}) === \count(\${$valueParam}->$propertyName) && !\array_udiff_assoc(\$this->{$propertyName}, \${$valueParam}->$propertyName, \$compareValues))"; 111 | }) 112 | ); 113 | 114 | if ($testsForMixedProperties) { 115 | return <<getTestsCode($testsForTypedProperties)}; 117 | if (!\$typedPropertiesAreEqual) { 118 | return false; 119 | } 120 | \$compareValues = static function (\$value1, \$value2) use (&\$compareValues) { 121 | if (\is_array(\$value1)) { 122 | \$equal = \is_array(\$value2) && \count(\$value1) === \count(\$value2) && !\array_udiff_assoc(\$value1, \$value2, \$compareValues); 123 | } else { 124 | \$equal = \$value1 === \$value2 || (\method_exists(\$value1, 'equals') ? \$value1->equals(\$value2) : \is_object(\$value1) && \$value1 == \$value2); 125 | } 126 | return \$equal ? 0 : 1; 127 | }; 128 | return {$this->getTestsCode($testsForMixedProperties)}; 129 | THEPHP; 130 | } else { 131 | return <<getTestsCode($testsForTypedProperties)}; 133 | THEPHP; 134 | } 135 | } 136 | 137 | private function isValueObject(ReflectionClass $templateClass, Property $property): bool 138 | { 139 | $reflectionClass = getClass($templateClass, $property->phpType()); 140 | return $reflectionClass->hasMethod('equals') 141 | && $this->matchMethod($reflectionClass->getMethod('equals')); 142 | } 143 | 144 | private function getTestsCode(array $tests): string 145 | { 146 | return \implode("\n && ", $tests); 147 | } 148 | } -------------------------------------------------------------------------------- /docs/howto.md: -------------------------------------------------------------------------------- 1 | # How do I... 2 | 3 | This page answers common how-to questions that may come up when using AutoValue. 4 | You should read and understand the [Introduction](index.md) first. 5 | 6 | Questions specific to usage of the **builder option** are documented separately; 7 | for this, start by reading [AutoValue with builders](builders.md). 8 | 9 | ## Contents 10 | 11 | How do I... 12 | 13 | * ... [also generate a **builder** for my value class?](#builder) 14 | * ... [include `with-` methods on my value class for creating slightly 15 | **altered** instances?](#withers) 16 | * ... [use (or not use) JavaBeans-style name **prefixes**?](#beans) 17 | * ... [use **nullable** properties?](#nullable) 18 | * ... [perform other **validation**?](#validate) 19 | * ... [use a property of a **mutable** type?](#mutable_property) 20 | * ... [use a **custom** implementation of `equals`, etc.?](#custom) 21 | * ... [have multiple **`create`** methods, or name it/them 22 | differently?](#create) 23 | * ... [**ignore** certain properties in `equals`, etc.?](#ignore) 24 | * ... [have AutoValue also implement abstract methods from my 25 | **supertypes**?](#supertypes) 26 | * ... [also include **setter** (mutator) methods?](#setters) 27 | * ... [have one `@AutoValue` class **extend** another?](#inherit) 28 | * ... [keep my accessor methods **private**?](#private_accessors) 29 | * ... [expose a **constructor**, not factory method, as my public creation 30 | API?](#public_constructor) 31 | * ... [use AutoValue on an **interface**, not abstract class?](#interface) 32 | * ... [**memoize** ("cache") derived properties?](#memoize) 33 | 34 | ## ... also generate a builder for my value class? 35 | 36 | Please see [AutoValue with builders](builders.md). 37 | 38 | ## ... include `with-` methods on my value class for creating slightly altered instances? 39 | 40 | This is a somewhat common pattern among immutable classes. You can't have 41 | setters, but you can have methods that act similarly to setters by returning a 42 | new immutable instance that has one property changed. 43 | 44 | To add a wither to your class, simply write the abstract method and AutoValue 45 | will generate the concrete method for you. 46 | 47 | ```php 48 | /** 49 | * @AutoValue 50 | */ 51 | abstract class Animal 52 | { 53 | public static function create(String $name, int $numberOfLegs): self 54 | { 55 | return new AutoValue_Animal([ 56 | 'name' => $name, 57 | 'numberOfLegs' => $numberOfLegs, 58 | ]); 59 | } 60 | 61 | abstract function name(): string; 62 | abstract function withName(string $name): self; 63 | abstract function numberOfLegs(): int; 64 | abstract function equals($value): bool; 65 | } 66 | ``` 67 | 68 | Note that it's your free choice whether to make `withName` public or protected. 69 | 70 | ## ... use (or not use) JavaBeans-style name prefixes? 71 | 72 | Some developers prefer to name their accessors with a `get-` or `is-` prefix, 73 | but would prefer that only the "bare" property name be used in `toString` and 74 | for the generated constructor's parameter names. 75 | 76 | AutoValue will do exactly this, but only if you are using these prefixes 77 | *consistently*. In that case, it infers your intended property name by first 78 | stripping the `get-` or `is-` prefix, then adjusting the case of what remains 79 | using [lcfirst()](http://php.net/manual/en/function.lcfirst.php). 80 | 81 | Note that, in keeping with the JavaBeans specification, the `is-` prefix is only 82 | allowed on `boolean`-returning methods. `get-` is allowed on any type of 83 | accessor. 84 | 85 | ## ... use nullable properties? 86 | 87 | If you want to allow null values for a property, simply use PHP 7.1's nullable 88 | parameter and return types where applicable. Example: 89 | 90 | ```php 91 | /** 92 | * @AutoValue 93 | */ 94 | abstract class Foo 95 | { 96 | static function create(?Bar $bar): self 97 | { 98 | return new AutoValue_Foo(['bar' => $bar]); 99 | } 100 | 101 | abstract function bar(): ?Bar; 102 | } 103 | ``` 104 | 105 | ## ... perform other validation? 106 | 107 | For precondition checks or pre-processing, just add them to your factory method: 108 | 109 | ```php 110 | static function create(string $first, string $second): self 111 | { 112 | assert(!empty($first)); 113 | return new AutoValue_MyType(['first' => $first, 'second' => trim($second)]); 114 | } 115 | ``` 116 | 117 | ## ... use a property of a mutable type? 118 | 119 | AutoValue classes are meant and expected to be immutable. But sometimes you 120 | would want to take a mutable type and use it as a property. In these cases: 121 | 122 | First, check if the mutable type has a corresponding immutable cousin. For 123 | example, the `DateTime` has an immutable counterpart `DateTimeImmutable`. If so, 124 | use the immutable type for your property, and only accept the mutable type 125 | during construction: 126 | 127 | ```php 128 | /** 129 | * @AutoValue 130 | */ 131 | abstract class DateTimeExample 132 | { 133 | static function create(DateTime $date): self 134 | { 135 | return new AutoValue_DateTimeExample(['date' => DateTimeImmutable::fromMutable($date)]); 136 | } 137 | 138 | abstract function date(): DateTimeImmutable; 139 | } 140 | ``` 141 | 142 | Note: this is a perfectly sensible practice, not an ugly workaround! 143 | 144 | If there is no suitable immutable type to use, you'll need to proceed with 145 | caution. Your static factory method should pass a *clone* of the passed object 146 | to the generated constructor. Your accessor method should document a very loud 147 | warning never to mutate the object returned. 148 | 149 | ```php 150 | /** 151 | * @AutoValue 152 | */ 153 | abstract class MutableExample 154 | { 155 | static function create(MutablePropertyType $ouch): self 156 | { 157 | // Replace `clone` below with the right copying code for this type 158 | return new AutoValue_MutableExample(['ouch' => clone $ouch]); 159 | } 160 | 161 | /** 162 | * Returns the ouch associated with this object; do not mutate the 163 | * returned object. 164 | */ 165 | abstract function ouch(): MutablePropertyType; 166 | } 167 | ``` 168 | 169 | Warning: this is an ugly workaround, not a perfectly sensible practice! Callers 170 | can trivially break the invariants of the immutable class by mutating the 171 | accessor's return value. An example where something can go wrong: AutoValue 172 | objects can be used as keys in Maps. 173 | 174 | ## ... use a custom implementation of `equals`, etc.? 175 | 176 | Simply write your custom implementation; AutoValue will notice this and will 177 | skip generating its own. Your hand-written logic will thus be inherited on the 178 | concrete implementation class. We call this *underriding* the method. 179 | 180 | Best practice: mark your underriding methods `final` to make it clear to future 181 | readers that these methods aren't overridden by AutoValue. 182 | 183 | ## ... have multiple `create` methods, or name it/them differently? 184 | 185 | Just do it! AutoValue doesn't actually care. This 186 | [best practice item](practices.md#one_reference) may be relevant. 187 | 188 | ## ... ignore certain properties in `equals`? 189 | 190 | Suppose your value class has an extra field that shouldn't be included in 191 | `equals`. 192 | 193 | If this is because it is a derived value based on other properties, see [How do 194 | I memoize derived properties?](#memoize). 195 | 196 | Otherwise, first make certain that you really want to do this. It is often, but 197 | not always, a mistake. Remember that libraries will treat two equal instances as 198 | absolutely *interchangeable* with each other. Whatever information is present in 199 | this extra field could essentially "disappear" when you aren't expecting it, for 200 | example when your value is stored and retrieved from certain collections. 201 | 202 | If you're sure, here is how to do it: 203 | 204 | ```php 205 | /** 206 | * @AutoValue 207 | */ 208 | abstract class IgnoreExample 209 | { 210 | static function create(string $normalProperty, string $ignoredProperty): self 211 | { 212 | $ie = new AutoValue_IgnoreExample(['normalProperty' => $normalProperty]); 213 | $ie->ignoredProperty = $ignoredProperty; 214 | return $ie; 215 | } 216 | 217 | abstract function normalProperty(): string; 218 | 219 | private $ignoredProperty; 220 | 221 | final public function ignoredProperty(): string 222 | { 223 | return $this->ignoredProperty; 224 | } 225 | } 226 | ``` 227 | 228 | ## ... have AutoValue also implement abstract methods from my supertypes? 229 | 230 | AutoValue will recognize every abstract accessor method whether it is defined 231 | directly in your own hand-written class or in a supertype. 232 | 233 | ## ... also include setter (mutator) methods? 234 | 235 | You can't; AutoValue only generates immutable value classes. 236 | 237 | Note that giving value semantics to a mutable type is widely considered a 238 | questionable practice in the first place. Equal instances of a value class are 239 | treated as *interchangeable*, but they can't truly be interchangeable if one 240 | might be mutated and the other not. 241 | 242 | ## ... have one `@AutoValue` class extend another? 243 | 244 | This ability is intentionally not supported, because there is no way to do it 245 | correctly. See *Effective Java, 2nd Edition* Item 8: "Obey the general contract 246 | when overriding equals". 247 | 248 | ## ... keep my accessor methods private? 249 | 250 | We're sorry. This is one of the rare and unfortunate restrictions AutoValue's 251 | approach places on your API. Your accessor methods don't have to be *public*, 252 | but they must be at least protected. 253 | 254 | ## ... expose a constructor, not factory method, as my public creation API? 255 | 256 | We're sorry. This is one of the rare restrictions AutoValue's approach places on 257 | your API. However, note that static factory methods are recommended over public 258 | constructors by *Effective Java*, Item 1. 259 | 260 | ## ... use AutoValue on an interface, not abstract class? 261 | 262 | Interfaces are not allowed. The only advantage of interfaces we're aware of is 263 | that you can omit `abstract` from the methods. That's not much. On the 264 | other hand, you would lose the immutability guarantee, and you'd also invite 265 | more of the kind of bad behavior described in [this best-practices 266 | item](practices.md#simple). On balance, we don't think it's worth it. 267 | 268 | ## ... memoize ("cache") derived properties? 269 | 270 | Sometimes your class has properties that are derived from the ones that 271 | AutoValue implements. You'd typically implement them with a concrete method that 272 | uses the other properties: 273 | 274 | ```php 275 | /** 276 | * @AutoValue 277 | */ 278 | abstract class Foo 279 | { 280 | abstract function barProperty(): Bar; 281 | 282 | function derivedProperty(): string 283 | { 284 | return someFunctionOf($this->barProperty()); 285 | } 286 | } 287 | ``` 288 | 289 | But what if `someFunctionOf(Bar)` is expensive? You'd like to calculate it only 290 | one time, then cache and reuse that value for all future calls. Normally, 291 | lazy initialization involves a bit of boilerplate. 292 | 293 | Instead, just write the derived-property accessor method as above, and 294 | annotate it with `@Memoized`. Then AutoValue will override that method to 295 | return a stored value after the first call: 296 | 297 | ```php 298 | /** 299 | * @AutoValue 300 | */ 301 | abstract class Foo 302 | { 303 | abstract function barProperty(): Bar; 304 | 305 | /** 306 | * @Memoized 307 | */ 308 | function derivedProperty(): string 309 | { 310 | return someFunctionOf($this->barProperty()); 311 | } 312 | } 313 | ``` 314 | 315 | Then your method will be called at most once. 316 | 317 | The annotated method must have the usual form of an accessor method, and may not 318 | be `abstract`, `final`, or `private`. 319 | 320 | The stored value will not be used in the implementation of `equals`. 321 | -------------------------------------------------------------------------------- /docs/builders-howto.md: -------------------------------------------------------------------------------- 1 | # How do I... (Builder edition) 2 | 3 | This page answers common how-to questions that may come up when using AutoValue 4 | **with the builder option**. You should read and understand [AutoValue with 5 | builders](builders.md) first. 6 | 7 | If you are not using a builder, see [Introduction](index.md) and 8 | [How do I...](howto.md) instead. 9 | 10 | ## Contents 11 | 12 | How do I... 13 | 14 | * ... [use (or not use) `set` **prefixes**?](#beans) 15 | * ... [use different **names** besides 16 | `builder()`/`Builder`/`build()`?](#build_names) 17 | * ... [specify a **default** value for a property?](#default) 18 | * ... [initialize a builder to the same property values as an **existing** 19 | value instance](#to_builder) 20 | * ... [**validate** property values?](#validate) 21 | 22 | ## ... use (or not use) `set` prefixes? 23 | 24 | Just as you can choose whether to use JavaBeans-style names for property getters 25 | (`getFoo()` or just `foo()`) in your value class, you have the same choice for 26 | setters in builders too (`setFoo(value)` or just `foo(value)`). As with getters, 27 | you must use these prefixes consistently or not at all. 28 | 29 | Using `get`/`is` prefixes for getters and using the `set` prefix for setters are 30 | independent choices. For example, it is fine to use the `set` prefixes on all 31 | your builder methods, but omit the `get`/`is` prefixes from all your accessors. 32 | 33 | Here is the `Animal` example using `get` prefixes but not `set` prefixes: 34 | 35 | ```php 36 | /** 37 | * @AutoValue 38 | */ 39 | abstract class Animal 40 | { 41 | abstract function getName(): string; 42 | abstract function getNumberOfLegs(): int; 43 | 44 | static function builder(): AnimalBuilder 45 | { 46 | return new AutoValue_AnimalBuilder(); 47 | } 48 | } 49 | 50 | /** 51 | * @AutoValue\Builder 52 | */ 53 | abstract class AnimalBuilder 54 | { 55 | abstract function name(string $value): self; 56 | abstract function numberOfLegs(int $value): self; 57 | abstract function build(): Animal; 58 | } 59 | ``` 60 | 61 | ## ... use different names besides `builder()`/`Builder`/`build()`? 62 | 63 | Use whichever names you like; AutoValue doesn't actually care. 64 | 65 | (We would gently recommend these names as conventional.) 66 | 67 | ## ... specify a default value for a property? 68 | 69 | What should happen when a caller does not supply a value for a property before 70 | calling `build()`? If the property in question is [nullable](howto.md#nullable), 71 | it will simply default to `null` as you would expect. But if is not nullable, 72 | then `build()` will throw an exception. 73 | 74 | But this presents a problem, since one of the main *advantages* of a builder in 75 | the first place is that callers can specify only the properties they care about! 76 | 77 | The solution is to provide a default value for such properties. Fortunately this 78 | is easy: just set it on the newly-constructed builder instance before returning 79 | it from the `builder()` method. 80 | 81 | Here is the `Animal` example with the default number of legs being 4: 82 | 83 | ```php 84 | /** 85 | * @AutoValue 86 | */ 87 | abstract class Animal 88 | { 89 | abstract function name(): string; 90 | abstract function numberOfLegs(): int; 91 | 92 | static function builder(): AnimalBuilder 93 | { 94 | return new AutoValue_AnimalBuilder()->setNumberOfLegs(4); 95 | } 96 | } 97 | ``` 98 | 99 | Occasionally you may want to supply a default value, but only if the property is 100 | not set explicitly. This is covered in the section on 101 | [normalization](#normalize). 102 | 103 | ## ... initialize a builder to the same property values as an existing value instance 104 | 105 | Suppose your caller has an existing instance of your value class, and wants to 106 | change only one or two of its properties. Of course, it's immutable, but it 107 | would be convenient if they could easily get a `Builder` instance representing 108 | the same property values, which they could then modify and use to create a new 109 | value instance. 110 | 111 | To give them this ability, just add an abstract `toBuilder` method, returning 112 | your abstract builder type, to your value class. AutoValue will implement it. 113 | 114 | ```php 115 | abstract function toBuilder(): FooBuilder; 116 | ``` 117 | 118 | ## ... validate property values? 119 | 120 | Validating properties is a little less straightforward than it is in the 121 | [non-builder case](howto.md#validate). 122 | 123 | What you need to do is *split* your "build" method into two methods: 124 | 125 | * the non-visible, abstract method that AutoValue implements 126 | * and the visible, *concrete* method you provide, which calls the generated 127 | method and performs validation. 128 | 129 | We recommend naming these methods `autoBuild` and `build`, but any names will 130 | work. It ends up looking like this: 131 | 132 | ```php 133 | /** 134 | * @AutoValue\Builder 135 | */ 136 | abstract class AnimalBuilder { 137 | abstract function name(string $value): self; 138 | abstract function numberOfLegs(int $value): self; 139 | 140 | function build(): Animal 141 | { 142 | $animal = $this->autoBuild(); 143 | assert($animal->numberOfLegs() >= 0, "Negative legs"); 144 | return $animal; 145 | } 146 | 147 | protected abstract function autoBuild(): Animal; 148 | } 149 | ``` 150 | 151 | -------------------------------------------------------------------------------- /tests/Console/Build/templates/vendor/composer/ClassLoader.php: -------------------------------------------------------------------------------- 1 | 7 | * Jordi Boggiano 8 | * 9 | * For the full copyright and license information, please view the LICENSE 10 | * file that was distributed with this source code. 11 | */ 12 | 13 | namespace Composer\Autoload; 14 | 15 | /** 16 | * ClassLoader implements a PSR-0, PSR-4 and classmap class loader. 17 | * 18 | * $loader = new \Composer\Autoload\ClassLoader(); 19 | * 20 | * // register classes with namespaces 21 | * $loader->add('Symfony\Component', __DIR__.'/component'); 22 | * $loader->add('Symfony', __DIR__.'/framework'); 23 | * 24 | * // activate the autoloader 25 | * $loader->register(); 26 | * 27 | * // to enable searching the include path (eg. for PEAR packages) 28 | * $loader->setUseIncludePath(true); 29 | * 30 | * In this example, if you try to use a class in the Symfony\Component 31 | * namespace or one of its children (Symfony\Component\Console for instance), 32 | * the autoloader will first look for the class under the component/ 33 | * directory, and it will then fallback to the framework/ directory if not 34 | * found before giving up. 35 | * 36 | * This class is loosely based on the Symfony UniversalClassLoader. 37 | * 38 | * @author Fabien Potencier 39 | * @author Jordi Boggiano 40 | * @see http://www.php-fig.org/psr/psr-0/ 41 | * @see http://www.php-fig.org/psr/psr-4/ 42 | */ 43 | class ClassLoader 44 | { 45 | // PSR-4 46 | private $prefixLengthsPsr4 = array(); 47 | private $prefixDirsPsr4 = array(); 48 | private $fallbackDirsPsr4 = array(); 49 | 50 | // PSR-0 51 | private $prefixesPsr0 = array(); 52 | private $fallbackDirsPsr0 = array(); 53 | 54 | private $useIncludePath = false; 55 | private $classMap = array(); 56 | private $classMapAuthoritative = false; 57 | private $missingClasses = array(); 58 | private $apcuPrefix; 59 | 60 | public function getPrefixes() 61 | { 62 | if (!empty($this->prefixesPsr0)) { 63 | return call_user_func_array('array_merge', $this->prefixesPsr0); 64 | } 65 | 66 | return array(); 67 | } 68 | 69 | public function getPrefixesPsr4() 70 | { 71 | return $this->prefixDirsPsr4; 72 | } 73 | 74 | public function getFallbackDirs() 75 | { 76 | return $this->fallbackDirsPsr0; 77 | } 78 | 79 | public function getFallbackDirsPsr4() 80 | { 81 | return $this->fallbackDirsPsr4; 82 | } 83 | 84 | public function getClassMap() 85 | { 86 | return $this->classMap; 87 | } 88 | 89 | /** 90 | * @param array $classMap Class to filename map 91 | */ 92 | public function addClassMap(array $classMap) 93 | { 94 | if ($this->classMap) { 95 | $this->classMap = array_merge($this->classMap, $classMap); 96 | } else { 97 | $this->classMap = $classMap; 98 | } 99 | } 100 | 101 | /** 102 | * Registers a set of PSR-0 directories for a given prefix, either 103 | * appending or prepending to the ones previously set for this prefix. 104 | * 105 | * @param string $prefix The prefix 106 | * @param array|string $paths The PSR-0 root directories 107 | * @param bool $prepend Whether to prepend the directories 108 | */ 109 | public function add($prefix, $paths, $prepend = false) 110 | { 111 | if (!$prefix) { 112 | if ($prepend) { 113 | $this->fallbackDirsPsr0 = array_merge( 114 | (array) $paths, 115 | $this->fallbackDirsPsr0 116 | ); 117 | } else { 118 | $this->fallbackDirsPsr0 = array_merge( 119 | $this->fallbackDirsPsr0, 120 | (array) $paths 121 | ); 122 | } 123 | 124 | return; 125 | } 126 | 127 | $first = $prefix[0]; 128 | if (!isset($this->prefixesPsr0[$first][$prefix])) { 129 | $this->prefixesPsr0[$first][$prefix] = (array) $paths; 130 | 131 | return; 132 | } 133 | if ($prepend) { 134 | $this->prefixesPsr0[$first][$prefix] = array_merge( 135 | (array) $paths, 136 | $this->prefixesPsr0[$first][$prefix] 137 | ); 138 | } else { 139 | $this->prefixesPsr0[$first][$prefix] = array_merge( 140 | $this->prefixesPsr0[$first][$prefix], 141 | (array) $paths 142 | ); 143 | } 144 | } 145 | 146 | /** 147 | * Registers a set of PSR-4 directories for a given namespace, either 148 | * appending or prepending to the ones previously set for this namespace. 149 | * 150 | * @param string $prefix The prefix/namespace, with trailing '\\' 151 | * @param array|string $paths The PSR-4 base directories 152 | * @param bool $prepend Whether to prepend the directories 153 | * 154 | * @throws \InvalidArgumentException 155 | */ 156 | public function addPsr4($prefix, $paths, $prepend = false) 157 | { 158 | if (!$prefix) { 159 | // Register directories for the root namespace. 160 | if ($prepend) { 161 | $this->fallbackDirsPsr4 = array_merge( 162 | (array) $paths, 163 | $this->fallbackDirsPsr4 164 | ); 165 | } else { 166 | $this->fallbackDirsPsr4 = array_merge( 167 | $this->fallbackDirsPsr4, 168 | (array) $paths 169 | ); 170 | } 171 | } elseif (!isset($this->prefixDirsPsr4[$prefix])) { 172 | // Register directories for a new namespace. 173 | $length = strlen($prefix); 174 | if ('\\' !== $prefix[$length - 1]) { 175 | throw new \InvalidArgumentException("A non-empty PSR-4 prefix must end with a namespace separator."); 176 | } 177 | $this->prefixLengthsPsr4[$prefix[0]][$prefix] = $length; 178 | $this->prefixDirsPsr4[$prefix] = (array) $paths; 179 | } elseif ($prepend) { 180 | // Prepend directories for an already registered namespace. 181 | $this->prefixDirsPsr4[$prefix] = array_merge( 182 | (array) $paths, 183 | $this->prefixDirsPsr4[$prefix] 184 | ); 185 | } else { 186 | // Append directories for an already registered namespace. 187 | $this->prefixDirsPsr4[$prefix] = array_merge( 188 | $this->prefixDirsPsr4[$prefix], 189 | (array) $paths 190 | ); 191 | } 192 | } 193 | 194 | /** 195 | * Registers a set of PSR-0 directories for a given prefix, 196 | * replacing any others previously set for this prefix. 197 | * 198 | * @param string $prefix The prefix 199 | * @param array|string $paths The PSR-0 base directories 200 | */ 201 | public function set($prefix, $paths) 202 | { 203 | if (!$prefix) { 204 | $this->fallbackDirsPsr0 = (array) $paths; 205 | } else { 206 | $this->prefixesPsr0[$prefix[0]][$prefix] = (array) $paths; 207 | } 208 | } 209 | 210 | /** 211 | * Registers a set of PSR-4 directories for a given namespace, 212 | * replacing any others previously set for this namespace. 213 | * 214 | * @param string $prefix The prefix/namespace, with trailing '\\' 215 | * @param array|string $paths The PSR-4 base directories 216 | * 217 | * @throws \InvalidArgumentException 218 | */ 219 | public function setPsr4($prefix, $paths) 220 | { 221 | if (!$prefix) { 222 | $this->fallbackDirsPsr4 = (array) $paths; 223 | } else { 224 | $length = strlen($prefix); 225 | if ('\\' !== $prefix[$length - 1]) { 226 | throw new \InvalidArgumentException("A non-empty PSR-4 prefix must end with a namespace separator."); 227 | } 228 | $this->prefixLengthsPsr4[$prefix[0]][$prefix] = $length; 229 | $this->prefixDirsPsr4[$prefix] = (array) $paths; 230 | } 231 | } 232 | 233 | /** 234 | * Turns on searching the include path for class files. 235 | * 236 | * @param bool $useIncludePath 237 | */ 238 | public function setUseIncludePath($useIncludePath) 239 | { 240 | $this->useIncludePath = $useIncludePath; 241 | } 242 | 243 | /** 244 | * Can be used to check if the autoloader uses the include path to check 245 | * for classes. 246 | * 247 | * @return bool 248 | */ 249 | public function getUseIncludePath() 250 | { 251 | return $this->useIncludePath; 252 | } 253 | 254 | /** 255 | * Turns off searching the prefix and fallback directories for classes 256 | * that have not been registered with the class map. 257 | * 258 | * @param bool $classMapAuthoritative 259 | */ 260 | public function setClassMapAuthoritative($classMapAuthoritative) 261 | { 262 | $this->classMapAuthoritative = $classMapAuthoritative; 263 | } 264 | 265 | /** 266 | * Should class lookup fail if not found in the current class map? 267 | * 268 | * @return bool 269 | */ 270 | public function isClassMapAuthoritative() 271 | { 272 | return $this->classMapAuthoritative; 273 | } 274 | 275 | /** 276 | * APCu prefix to use to cache found/not-found classes, if the extension is enabled. 277 | * 278 | * @param string|null $apcuPrefix 279 | */ 280 | public function setApcuPrefix($apcuPrefix) 281 | { 282 | $this->apcuPrefix = function_exists('apcu_fetch') && ini_get('apc.enabled') ? $apcuPrefix : null; 283 | } 284 | 285 | /** 286 | * The APCu prefix in use, or null if APCu caching is not enabled. 287 | * 288 | * @return string|null 289 | */ 290 | public function getApcuPrefix() 291 | { 292 | return $this->apcuPrefix; 293 | } 294 | 295 | /** 296 | * Registers this instance as an autoloader. 297 | * 298 | * @param bool $prepend Whether to prepend the autoloader or not 299 | */ 300 | public function register($prepend = false) 301 | { 302 | spl_autoload_register(array($this, 'loadClass'), true, $prepend); 303 | } 304 | 305 | /** 306 | * Unregisters this instance as an autoloader. 307 | */ 308 | public function unregister() 309 | { 310 | spl_autoload_unregister(array($this, 'loadClass')); 311 | } 312 | 313 | /** 314 | * Loads the given class or interface. 315 | * 316 | * @param string $class The name of the class 317 | * @return bool|null True if loaded, null otherwise 318 | */ 319 | public function loadClass($class) 320 | { 321 | if ($file = $this->findFile($class)) { 322 | includeFile($file); 323 | 324 | return true; 325 | } 326 | } 327 | 328 | /** 329 | * Finds the path to the file where the class is defined. 330 | * 331 | * @param string $class The name of the class 332 | * 333 | * @return string|false The path if found, false otherwise 334 | */ 335 | public function findFile($class) 336 | { 337 | // class map lookup 338 | if (isset($this->classMap[$class])) { 339 | return $this->classMap[$class]; 340 | } 341 | if ($this->classMapAuthoritative || isset($this->missingClasses[$class])) { 342 | return false; 343 | } 344 | if (null !== $this->apcuPrefix) { 345 | $file = apcu_fetch($this->apcuPrefix.$class, $hit); 346 | if ($hit) { 347 | return $file; 348 | } 349 | } 350 | 351 | $file = $this->findFileWithExtension($class, '.php'); 352 | 353 | // Search for Hack files if we are running on HHVM 354 | if (false === $file && defined('HHVM_VERSION')) { 355 | $file = $this->findFileWithExtension($class, '.hh'); 356 | } 357 | 358 | if (null !== $this->apcuPrefix) { 359 | apcu_add($this->apcuPrefix.$class, $file); 360 | } 361 | 362 | if (false === $file) { 363 | // Remember that this class does not exist. 364 | $this->missingClasses[$class] = true; 365 | } 366 | 367 | return $file; 368 | } 369 | 370 | private function findFileWithExtension($class, $ext) 371 | { 372 | // PSR-4 lookup 373 | $logicalPathPsr4 = strtr($class, '\\', DIRECTORY_SEPARATOR) . $ext; 374 | 375 | $first = $class[0]; 376 | if (isset($this->prefixLengthsPsr4[$first])) { 377 | $subPath = $class; 378 | while (false !== $lastPos = strrpos($subPath, '\\')) { 379 | $subPath = substr($subPath, 0, $lastPos); 380 | $search = $subPath.'\\'; 381 | if (isset($this->prefixDirsPsr4[$search])) { 382 | $pathEnd = DIRECTORY_SEPARATOR . substr($logicalPathPsr4, $lastPos + 1); 383 | foreach ($this->prefixDirsPsr4[$search] as $dir) { 384 | if (file_exists($file = $dir . $pathEnd)) { 385 | return $file; 386 | } 387 | } 388 | } 389 | } 390 | } 391 | 392 | // PSR-4 fallback dirs 393 | foreach ($this->fallbackDirsPsr4 as $dir) { 394 | if (file_exists($file = $dir . DIRECTORY_SEPARATOR . $logicalPathPsr4)) { 395 | return $file; 396 | } 397 | } 398 | 399 | // PSR-0 lookup 400 | if (false !== $pos = strrpos($class, '\\')) { 401 | // namespaced class name 402 | $logicalPathPsr0 = substr($logicalPathPsr4, 0, $pos + 1) 403 | . strtr(substr($logicalPathPsr4, $pos + 1), '_', DIRECTORY_SEPARATOR); 404 | } else { 405 | // PEAR-like class name 406 | $logicalPathPsr0 = strtr($class, '_', DIRECTORY_SEPARATOR) . $ext; 407 | } 408 | 409 | if (isset($this->prefixesPsr0[$first])) { 410 | foreach ($this->prefixesPsr0[$first] as $prefix => $dirs) { 411 | if (0 === strpos($class, $prefix)) { 412 | foreach ($dirs as $dir) { 413 | if (file_exists($file = $dir . DIRECTORY_SEPARATOR . $logicalPathPsr0)) { 414 | return $file; 415 | } 416 | } 417 | } 418 | } 419 | } 420 | 421 | // PSR-0 fallback dirs 422 | foreach ($this->fallbackDirsPsr0 as $dir) { 423 | if (file_exists($file = $dir . DIRECTORY_SEPARATOR . $logicalPathPsr0)) { 424 | return $file; 425 | } 426 | } 427 | 428 | // PSR-0 include paths. 429 | if ($this->useIncludePath && $file = stream_resolve_include_path($logicalPathPsr0)) { 430 | return $file; 431 | } 432 | 433 | return false; 434 | } 435 | } 436 | 437 | /** 438 | * Scope isolated include. 439 | * 440 | * Prevents access to $this/self from included files. 441 | */ 442 | function includeFile($file) 443 | { 444 | include $file; 445 | } 446 | --------------------------------------------------------------------------------