├── LICENSE.md ├── CHANGELOG.md ├── .php_cs.dist.php ├── composer.json ├── src ├── Method.php ├── Handlers.php ├── Type.php └── Attributes.php └── README.md /LICENSE.md: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) spatie 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in 13 | all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 21 | THE SOFTWARE. 22 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | All notable changes to `better-types` will be documented in this file. 4 | 5 | ## 1.0.1 - 2025-03-08 6 | 7 | ### What's Changed 8 | 9 | * Small improvements by @maartenpaauw in https://github.com/spatie/better-types/pull/7 10 | * Add support for union types by @maartenpaauw in https://github.com/spatie/better-types/pull/8 11 | 12 | ### New Contributors 13 | 14 | * @maartenpaauw made their first contribution in https://github.com/spatie/better-types/pull/7 15 | 16 | **Full Changelog**: https://github.com/spatie/better-types/compare/1.0.0...1.0.1 17 | 18 | ## 0.2.0 - 2024-03-08 19 | 20 | ### What's Changed 21 | 22 | * Allow Laravel 11 by @inmanturbo in https://github.com/spatie/better-types/pull/6 23 | 24 | ### New Contributors 25 | 26 | * @inmanturbo made their first contribution in https://github.com/spatie/better-types/pull/6 27 | 28 | **Full Changelog**: https://github.com/spatie/better-types/compare/0.1.3...0.2.0 29 | 30 | ## 0.1.3 - 2023-01-25 31 | 32 | Allow L10 collections 33 | 34 | ## 0.1.2 - 2022-07-18 35 | 36 | - Add accepts types method #4 37 | 38 | ## 0.1.1 - 2022-01-14 39 | 40 | - allow Laravel 9 41 | 42 | ## 1.0.0 - 2021-11-24 43 | 44 | - initial release 45 | -------------------------------------------------------------------------------- /.php_cs.dist.php: -------------------------------------------------------------------------------- 1 | in([ 5 | __DIR__ . '/src', 6 | __DIR__ . '/tests', 7 | ]) 8 | ->name('*.php') 9 | ->notName('*.blade.php') 10 | ->ignoreDotFiles(true) 11 | ->ignoreVCS(true); 12 | 13 | return (new PhpCsFixer\Config()) 14 | ->setRules([ 15 | '@PSR12' => true, 16 | 'array_syntax' => ['syntax' => 'short'], 17 | 'ordered_imports' => ['sort_algorithm' => 'alpha'], 18 | 'no_unused_imports' => true, 19 | 'not_operator_with_successor_space' => true, 20 | 'trailing_comma_in_multiline' => true, 21 | 'phpdoc_scalar' => true, 22 | 'unary_operator_spaces' => true, 23 | 'binary_operator_spaces' => true, 24 | 'blank_line_before_statement' => [ 25 | 'statements' => ['break', 'continue', 'declare', 'return', 'throw', 'try'], 26 | ], 27 | 'phpdoc_single_line_var_spacing' => true, 28 | 'phpdoc_var_without_name' => true, 29 | 'class_attributes_separation' => [ 30 | 'elements' => [ 31 | 'method' => 'one', 32 | ], 33 | ], 34 | 'method_argument_space' => [ 35 | 'on_multiline' => 'ensure_fully_multiline', 36 | 'keep_multiple_spaces_after_comma' => true, 37 | ], 38 | 'single_trait_insert_per_statement' => true, 39 | ]) 40 | ->setFinder($finder); 41 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "spatie/better-types", 3 | "description": "Improved abstraction for dealing with union and named types.", 4 | "keywords": [ 5 | "spatie", 6 | "better-types" 7 | ], 8 | "homepage": "https://github.com/spatie/better-types", 9 | "license": "MIT", 10 | "authors": [ 11 | { 12 | "name": "Brent Roose", 13 | "email": "brent@spatie.be", 14 | "role": "Developer" 15 | } 16 | ], 17 | "require": { 18 | "php": "^8.2", 19 | "illuminate/collections": "^10|^11.43.2|^12" 20 | }, 21 | "require-dev": { 22 | "friendsofphp/php-cs-fixer": "^3.69.1", 23 | "phpunit/phpunit": "^10.0|^11.5.8", 24 | "spatie/ray": "^1.41.5" 25 | }, 26 | "autoload": { 27 | "psr-4": { 28 | "Spatie\\BetterTypes\\": "src" 29 | } 30 | }, 31 | "autoload-dev": { 32 | "psr-4": { 33 | "Spatie\\BetterTypes\\Tests\\": "tests" 34 | } 35 | }, 36 | "scripts": { 37 | "psalm": "vendor/bin/psalm", 38 | "phpstan": "vendor/bin/phpstan analyse src tests -l4", 39 | "test": "vendor/bin/phpunit", 40 | "test-coverage": "vendor/bin/phpunit --coverage-html coverage", 41 | "format": "vendor/bin/php-cs-fixer fix --allow-risky=yes" 42 | }, 43 | "config": { 44 | "sort-packages": true 45 | }, 46 | "minimum-stability": "dev", 47 | "prefer-stable": true 48 | } 49 | -------------------------------------------------------------------------------- /src/Method.php: -------------------------------------------------------------------------------- 1 | getParameters() as $index => $parameter) { 26 | $type = new Type($parameter->getType()); 27 | 28 | $this->positionalTypes[$index] = $type; 29 | $this->namedTypes[$parameter->getName()] = $type; 30 | } 31 | 32 | $this->inputCount = count($this->positionalTypes); 33 | } 34 | 35 | public function accepts(mixed ...$input): bool 36 | { 37 | $types = array_is_list($input) ? $this->positionalTypes : $this->namedTypes; 38 | 39 | if (count($input) !== $this->inputCount) { 40 | return false; 41 | } 42 | 43 | foreach ($types as $index => $type) { 44 | $currentInput = $input[$index] ?? null; 45 | 46 | if (! $type->accepts($currentInput)) { 47 | return false; 48 | } 49 | } 50 | 51 | return true; 52 | } 53 | 54 | public function acceptsTypes(array $input): bool 55 | { 56 | if (count($input) !== $this->inputCount) { 57 | return false; 58 | } 59 | 60 | foreach ($this->positionalTypes as $index => $type) { 61 | if ($type->isUnion() && ! $type->unionContains($input[$index])) { 62 | return false; 63 | } elseif (! $type->isUnion() && ! $type->hasName($input[$index])) { 64 | return false; 65 | } 66 | } 67 | 68 | return true; 69 | } 70 | 71 | public function getName(): string 72 | { 73 | return $this->reflectionMethod->getName(); 74 | } 75 | 76 | public function getTypes(): Collection 77 | { 78 | return collect($this->namedTypes); 79 | } 80 | 81 | public function visibility(): string 82 | { 83 | return match (true) { 84 | $this->isPrivate() => self::PRIVATE, 85 | $this->isProtected() => self::PROTECTED, 86 | default => self::PUBLIC, 87 | }; 88 | } 89 | 90 | public function isStatic(): bool 91 | { 92 | return ($this->reflectionMethod->getModifiers() & ReflectionMethod::IS_STATIC) !== 0; 93 | } 94 | 95 | public function isFinal(): bool 96 | { 97 | return ($this->reflectionMethod->getModifiers() & ReflectionMethod::IS_FINAL) !== 0; 98 | } 99 | 100 | public function isAbstract(): bool 101 | { 102 | return ($this->reflectionMethod->getModifiers() & ReflectionMethod::IS_ABSTRACT) !== 0; 103 | } 104 | 105 | public function isPublic(): bool 106 | { 107 | return ($this->reflectionMethod->getModifiers() & ReflectionMethod::IS_PUBLIC) !== 0; 108 | } 109 | 110 | public function isProtected(): bool 111 | { 112 | return ($this->reflectionMethod->getModifiers() & ReflectionMethod::IS_PROTECTED) !== 0; 113 | } 114 | 115 | public function isPrivate(): bool 116 | { 117 | return ($this->reflectionMethod->getModifiers() & ReflectionMethod::IS_PRIVATE) !== 0; 118 | } 119 | } 120 | -------------------------------------------------------------------------------- /src/Handlers.php: -------------------------------------------------------------------------------- 1 | getMethods() as $reflectionMethod) { 32 | $this->methods[$reflectionMethod->getName()] = new Method($reflectionMethod); 33 | } 34 | } 35 | 36 | /** 37 | * @return iterable<\Spatie\BetterTypes\Method>|\Illuminate\Support\Collection 38 | */ 39 | public function all(): iterable|Collection 40 | { 41 | $allMethods = []; 42 | 43 | foreach ($this->methods as $name => $method) { 44 | if (! $this->filterAllows($method)) { 45 | continue; 46 | } 47 | 48 | $allMethods[$name] = $method; 49 | } 50 | 51 | return collect($allMethods); 52 | } 53 | 54 | public function first(): ?Method 55 | { 56 | return $this->all()->first(); 57 | } 58 | 59 | /** 60 | * @param Closure(\Spatie\BetterTypes\Method): bool $filter 61 | * 62 | * @return self 63 | */ 64 | public function filter(Closure $filter): self 65 | { 66 | $clone = clone $this; 67 | 68 | $clone->filters[] = $filter; 69 | 70 | return $clone; 71 | } 72 | 73 | /** 74 | * @param Closure(\Spatie\BetterTypes\Method): bool $reject 75 | * 76 | * @return self 77 | */ 78 | public function reject(Closure $reject): self 79 | { 80 | return $this->filter(fn (Method $method) => ! $reject($method)); 81 | } 82 | 83 | public function accepts(mixed ...$input): self 84 | { 85 | return $this->filter(fn (Method $method) => $method->accepts(...$input)); 86 | } 87 | 88 | /** 89 | * @param array $input 90 | * 91 | * @return self 92 | */ 93 | public function acceptsTypes(array $input): self 94 | { 95 | return $this->filter(fn (Method $method) => $method->acceptsTypes($input)); 96 | } 97 | 98 | public function public(): self 99 | { 100 | $clone = clone $this; 101 | 102 | $clone->visibilityFilter[] = Method::PUBLIC; 103 | 104 | return $clone; 105 | } 106 | 107 | public function protected(): self 108 | { 109 | $clone = clone $this; 110 | 111 | $clone->visibilityFilter[] = Method::PROTECTED; 112 | 113 | return $clone; 114 | } 115 | 116 | public function private(): self 117 | { 118 | $clone = clone $this; 119 | 120 | $clone->visibilityFilter[] = Method::PRIVATE; 121 | 122 | return $clone; 123 | } 124 | 125 | private function filterAllows(Method $method): bool 126 | { 127 | if ( 128 | $this->visibilityFilter !== [] 129 | && ! in_array($method->visibility(), $this->visibilityFilter) 130 | ) { 131 | return false; 132 | } 133 | 134 | foreach ($this->filters as $filter) { 135 | if ($filter($method) === false) { 136 | return false; 137 | } 138 | } 139 | 140 | return true; 141 | } 142 | } 143 | -------------------------------------------------------------------------------- /src/Type.php: -------------------------------------------------------------------------------- 1 | 'float', 14 | 'int' => 'integer', 15 | 'bool' => 'boolean', 16 | ]; 17 | 18 | private bool $isNullable = false; 19 | 20 | private bool $isMixed = false; 21 | 22 | /** 23 | * @var array 24 | */ 25 | private array $acceptedTypes = []; 26 | 27 | private string $name = ''; 28 | 29 | public static function new(null|ReflectionType|ReflectionParameter $reflection): self 30 | { 31 | if ($reflection instanceof ReflectionParameter) { 32 | $reflection = $reflection->getType(); 33 | } 34 | 35 | return new self($reflection); 36 | } 37 | 38 | public function __construct( 39 | null | ReflectionType $reflectionType 40 | ) { 41 | if ($reflectionType === null) { 42 | $this->isNullable = true; 43 | $this->isMixed = true; 44 | } 45 | 46 | if ($reflectionType instanceof ReflectionNamedType) { 47 | $this->acceptedTypes = [$this->normalize($reflectionType->getName())]; 48 | $this->isNullable = $reflectionType->allowsNull(); 49 | $this->isMixed = $reflectionType->getName() === 'mixed'; 50 | $this->name = $reflectionType->getName(); 51 | } 52 | 53 | if ($reflectionType instanceof ReflectionUnionType) { 54 | $names = []; 55 | 56 | foreach ($reflectionType->getTypes() as $namedType) { 57 | $this->acceptedTypes[] = $this->normalize($namedType->getName()); 58 | $this->isNullable = $this->isNullable || $namedType->allowsNull(); 59 | $this->isMixed = $namedType->getName() === 'mixed'; 60 | $names[] = $namedType->getName(); 61 | } 62 | 63 | $this->name = implode('|', $names); 64 | } 65 | } 66 | 67 | public function hasName(string $name): bool 68 | { 69 | return $this->getName() === $name; 70 | } 71 | 72 | public function getName(): string 73 | { 74 | return $this->name; 75 | } 76 | 77 | public function isUnion(): bool 78 | { 79 | return count($this->acceptedTypes) > 1; 80 | } 81 | 82 | public function unionContains(string $type): bool 83 | { 84 | return in_array($this->normalize($type), $this->acceptedTypes, true); 85 | } 86 | 87 | public function accepts(mixed $input): bool 88 | { 89 | if ($this->isMixed) { 90 | return true; 91 | } 92 | 93 | if ($this->isNullable && $input === null) { 94 | return true; 95 | } 96 | 97 | $inputType = $this->normalize(gettype($input)); 98 | 99 | if (in_array($inputType, $this->acceptedTypes)) { 100 | return true; 101 | } 102 | 103 | if ($inputType === 'object') { 104 | $interfaces = class_implements($input); 105 | 106 | $parents = class_parents($input); 107 | 108 | foreach ($this->acceptedTypes as $acceptedType) { 109 | $extendsOrIs = 110 | $input::class === $acceptedType 111 | || array_key_exists($acceptedType, $interfaces) 112 | || array_key_exists($acceptedType, $parents); 113 | 114 | if ($extendsOrIs) { 115 | return true; 116 | } 117 | } 118 | } 119 | 120 | return false; 121 | } 122 | 123 | private function normalize(?string $type): ?string 124 | { 125 | if ($type === null) { 126 | return null; 127 | } 128 | 129 | return self::$typeMapping[$type] ?? $type; 130 | } 131 | } 132 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Better types 2 | 3 | Check whether a reflection type or method accepts a given input 4 | 5 | [![Latest Version on Packagist](https://img.shields.io/packagist/v/spatie/better-types.svg?style=flat-square)](https://packagist.org/packages/spatie/better-types) 6 | [![GitHub Tests Action Status](https://img.shields.io/github/workflow/status/spatie/better-types/run-tests?label=tests)](https://github.com/spatie/better-types/actions?query=workflow%3ATests+branch%3Amaster) 7 | [![GitHub Code Style Action Status](https://img.shields.io/github/workflow/status/spatie/better-types/Check%20&%20fix%20styling?label=code%20style)](https://github.com/spatie/better-types/actions?query=workflow%3A"Check+%26+fix+styling"+branch%3Amaster) 8 | [![Total Downloads](https://img.shields.io/packagist/dt/spatie/better-types.svg?style=flat-square)](https://packagist.org/packages/spatie/better-types) 9 | 10 | ## Support us 11 | 12 | [](https://spatie.be/github-ad-click/better-types) 13 | 14 | We invest a lot of resources into creating [best in class open source packages](https://spatie.be/open-source). You can support us by [buying one of our paid products](https://spatie.be/open-source/support-us). 15 | 16 | We highly appreciate you sending us a postcard from your hometown, mentioning which of our package(s) you are using. You'll find our address on [our contact page](https://spatie.be/about-us). We publish all received postcards on [our virtual postcard wall](https://spatie.be/open-source/postcards). 17 | 18 | ## Installation 19 | 20 | You can install the package via composer: 21 | 22 | ```bash 23 | composer require spatie/better-types 24 | ``` 25 | 26 | ## Usage 27 | 28 | Using the `Type` class directly: 29 | 30 | ```php 31 | function (FooInterface $foo) {} 32 | 33 | $reflectionType = … 34 | 35 | $type = new Type($reflectionType); 36 | 37 | $type->accepts(new Foo()); // true 38 | $type->accepts('invalid string'); // false 39 | ``` 40 | 41 | Using the `Method` class: 42 | 43 | ```php 44 | function (?FooInterface $foo, ?BarInterface $bar) {} 45 | 46 | $reflectionMethod = … 47 | 48 | $method = new Method($reflectionMethod); 49 | 50 | $method->accepts(new Foo(), new Bar()); // true 51 | $method->accepts(bar: new Bar() foo: new Foo()); // true 52 | $method->accepts(null, new Bar()); // true 53 | $method->accepts(null, null); // true 54 | 55 | $method->accepts('string', 1); // false 56 | $method->accepts(new Foo()); // false, you can't omit values 57 | ``` 58 | 59 | Using `Handlers` to determine which methods accept a given set of input: 60 | 61 | ```php 62 | class Foo 63 | { 64 | public function acceptsString(string $a) {} 65 | 66 | public function acceptsStringToo(string $a) {} 67 | 68 | public function acceptsInt(int $a) {} 69 | } 70 | 71 | $reflectionClass = … 72 | 73 | $handlers = new Handlers($reflectionClass); 74 | 75 | $handlers->accepts('string')->all(); // ['acceptsString', 'acceptsStringToo'] 76 | $handlers->accepts(1)->first(); // 'acceptsInt' 77 | ``` 78 | 79 | Using the `Attributes` class to find and instantiate attributes with a fluent API: 80 | 81 | ```php 82 | Attributes::new(AttributesTestClass::class) 83 | ->instanceOf(AttributesTestAttribute::class) 84 | ->first(); 85 | ``` 86 | 87 | ## Testing 88 | 89 | ```bash 90 | composer test 91 | ``` 92 | 93 | ## Changelog 94 | 95 | Please see [CHANGELOG](CHANGELOG.md) for more information on what has changed recently. 96 | 97 | ## Contributing 98 | 99 | Please see [CONTRIBUTING](https://github.com/spatie/.github/blob/main/CONTRIBUTING.md) for details. 100 | 101 | ## Security Vulnerabilities 102 | 103 | Please review [our security policy](../../security/policy) on how to report security vulnerabilities. 104 | 105 | ## Credits 106 | 107 | - [Brent Roose](https://github.com/spatie) 108 | - [All Contributors](../../contributors) 109 | 110 | ## License 111 | 112 | The MIT License (MIT). Please see [License File](LICENSE.md) for more information. 113 | -------------------------------------------------------------------------------- /src/Attributes.php: -------------------------------------------------------------------------------- 1 | $className 50 | * 51 | * @return self 52 | */ 53 | public function instanceOf(string $className): self 54 | { 55 | /** @var self $clone */ 56 | $clone = clone $this; 57 | 58 | $clone->instanceOf = $className; 59 | 60 | return $clone; 61 | } 62 | 63 | /** 64 | * @return self 65 | */ 66 | public function asAttributes(): self 67 | { 68 | $clone = clone $this; 69 | 70 | $clone->asAttributes = true; 71 | 72 | return $clone; 73 | } 74 | 75 | /** 76 | * @param Closure(\ReflectionAttribute): bool $filter 77 | * 78 | * @return self 79 | */ 80 | public function filter(Closure $filter): self 81 | { 82 | $clone = clone $this; 83 | 84 | $clone->filters[] = $filter; 85 | 86 | return $clone; 87 | } 88 | 89 | /** 90 | * @param Closure(\ReflectionAttribute): bool $reject 91 | * 92 | * @return self 93 | */ 94 | public function reject(Closure $reject): self 95 | { 96 | return $this->filter(fn (ReflectionAttribute $attribute) => ! $reject($attribute)); 97 | } 98 | 99 | /** 100 | * @return iterable|\Illuminate\Support\Collection 101 | */ 102 | public function all(): iterable|Collection 103 | { 104 | $allAttributes = []; 105 | 106 | if ($this->instanceOf) { 107 | $attributes = $this->reflection->getAttributes($this->instanceOf, ReflectionAttribute::IS_INSTANCEOF); 108 | } else { 109 | $attributes = $this->reflection->getAttributes(); 110 | } 111 | 112 | foreach ($attributes as $attribute) { 113 | if (! $this->filterAllows($attribute)) { 114 | continue; 115 | } 116 | 117 | if (! $this->asAttributes) { 118 | $attribute = $attribute->newInstance(); 119 | } 120 | 121 | $allAttributes[] = $attribute; 122 | } 123 | 124 | return collect($allAttributes); 125 | } 126 | 127 | /** 128 | * @return AttributeType 129 | */ 130 | public function first(): mixed 131 | { 132 | $first = $this->asAttributes()->all()->first(); 133 | 134 | if (! $this->asAttributes) { 135 | $first = $first->newInstance(); 136 | } 137 | 138 | return $first; 139 | } 140 | 141 | private function filterAllows(ReflectionAttribute $attribute): bool 142 | { 143 | foreach ($this->filters as $filter) { 144 | if ($filter($attribute) === false) { 145 | return false; 146 | } 147 | } 148 | 149 | return true; 150 | } 151 | } 152 | --------------------------------------------------------------------------------