├── .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 | [](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 |
--------------------------------------------------------------------------------