├── .gitignore ├── docs ├── installing.md ├── current-limitation.md ├── parameter-checking.md ├── bootstrap.md ├── immutability.md ├── return-type-checks.md ├── property-type-checks.md ├── public-constructor-property-initialization-checks.md ├── parameter-interface-jailing.md └── basic-strict.md ├── tests ├── functional │ ├── int-typed-property-violation.phpt │ ├── string-typed-property-violation.phpt │ ├── integer-typed-property-violation.phpt │ ├── object-typed-property-violation.phpt │ ├── callable-typed-property-violation.phpt │ ├── array-typed-property-violation.phpt │ ├── uninitialized-parent-class-properties-violation.phpt │ ├── uninitialized-properties-in-constructor-violation.phpt │ ├── resource-typed-property-violation.phpt │ ├── return-string-type-violation.phpt │ ├── multiple-parameter-types-resolver.phpt │ ├── stdclass-typed-property-violation.phpt │ ├── return-self-type-violation.phpt │ ├── return-this-type-violation.phpt │ ├── return-static-type-violation.phpt │ ├── return-object-type-violation.phpt │ ├── self-typed-property-violation.phpt │ ├── return-stdclass-type-violation.phpt │ ├── static-typed-property-violation.phpt │ ├── immutable-property-violation.phpt │ ├── same-typed-property-violation.phpt │ ├── return-mixed-collection-type-violation.phpt │ ├── call-to-non-interfaced-method-violation.phpt │ ├── typed-array-property-violation.phpt │ ├── init.php │ ├── call-to-method-with-typed-array-violation.phpt │ └── call-to-method-typed-with-multiparams-violation.phpt ├── StrictPhpTestAsset │ ├── ClassWithGenericNonTypedProperty.php │ ├── ClassWithSelfTypedProperty.php │ ├── ClassWithStaticTypedProperty.php │ ├── ClassWithTypedArrayProperty.php │ ├── ClassWithCallableTypedProperty.php │ ├── ClassWithGenericIntTypedProperty.php │ ├── ClassWithMethodWithNoHints.php │ ├── ClassWithStdClassTypedProperty.php │ ├── ClassWithGenericArrayTypedProperty.php │ ├── ClassWithGenericIntegerTypedProperty.php │ ├── ClassWithGenericObjectTypedProperty.php │ ├── ClassWithGenericStringTypedProperty.php │ ├── ClassWithSameTypedProperty.php │ ├── ClassWithGenericResourceTypedProperty.php │ ├── ClassWithImmutableProperty.php │ ├── HelloInterface.php │ ├── ClassWithComplexParameterOnMethod.php │ ├── ClassWithMethodWithSelfHint.php │ ├── ClassWithMethodWithStaticHint.php │ ├── ClassWithTypedArrayMethodParameterAnnotation.php │ ├── ClassWithIncorrectlyInitializingConstructor.php │ ├── ClassHintingAgainstImportOfOwnNamespace.php │ ├── ParentClassWithInitializingConstructor.php │ ├── ClassWithMultipleParamsTypedMethodAnnotation.php │ ├── ClassWithHelloImplementationAndAdditionalMethod.php │ ├── ClassWithImportedHintClasses.php │ ├── ClassWithIncorrectlyInitializedParentClassProperties.php │ ├── ClassThatDependsOnHello.php │ ├── ClassWithReturnTypeMethod.php │ └── ClassWithVariadicInterfaceParameters.php └── StrictPhpTest │ ├── Reflection │ └── AllPropertiesTest.php │ ├── TypeChecker │ ├── ApplyTypeChecksTest.php │ └── TypeChecker │ │ ├── NullTypeCheckerTest.php │ │ ├── MixedTypeCheckerTest.php │ │ ├── IntegerTypeCheckerTest.php │ │ ├── BooleanTypeCheckerTest.php │ │ ├── ResourceTypeCheckerTest.php │ │ ├── CallableTypeCheckerTest.php │ │ ├── StringTypeCheckerTest.php │ │ └── GenericObjectTypeCheckerTest.php │ ├── Aspect │ ├── PrePublicMethodAspectTest.php │ ├── PostPublicMethodAspectTest.php │ ├── PropertyWriteAspectTest.php │ └── PostConstructAspectTest.php │ └── AccessChecker │ ├── PropertyWriteTypeCheckerTest.php │ └── ReturnTypeCheckerTest.php ├── phpunit.xml.dist ├── LICENSE ├── CONTRIBUTING.md ├── .scrutinizer.yml ├── .travis.yml ├── couscous.yml ├── src └── StrictPhp │ ├── TypeChecker │ ├── TypeCheckerInterface.php │ ├── TypeChecker │ │ ├── IntegerTypeChecker.php │ │ ├── StringTypeChecker.php │ │ ├── GenericObjectTypeChecker.php │ │ ├── CallableTypeChecker.php │ │ ├── MixedTypeChecker.php │ │ ├── BooleanTypeChecker.php │ │ ├── NullTypeChecker.php │ │ ├── ResourceTypeChecker.php │ │ ├── ObjectTypeChecker.php │ │ └── TypedTraversableChecker.php │ └── ApplyTypeChecks.php │ ├── Aspect │ ├── PrePublicMethodAspect.php │ ├── PropertyWriteAspect.php │ ├── PostPublicMethodAspect.php │ └── PostConstructAspect.php │ ├── Reflection │ └── AllProperties.php │ ├── AccessChecker │ ├── ReturnTypeChecker.php │ ├── PropertyWriteImmutabilityChecker.php │ ├── PropertyWriteTypeChecker.php │ ├── ObjectStateChecker.php │ ├── ParameterTypeChecker.php │ └── ParameterInterfaceJailer.php │ └── TypeFinder │ ├── PropertyTypeFinder.php │ ├── ReturnTypeFinder.php │ └── ParameterTypeFinder.php └── composer.json /.gitignore: -------------------------------------------------------------------------------- 1 | vendor 2 | composer.lock 3 | go-cache 4 | integration-tests-go-cache 5 | clover.xml -------------------------------------------------------------------------------- /docs/installing.md: -------------------------------------------------------------------------------- 1 | --- 2 | currentMenu: install 3 | --- 4 | 5 | # Installing 6 | 7 | Do you have to use `[composer](https://getcomposer.org)` to install this library. 8 | 9 | ```sh 10 | composer require roave/strict-php 11 | ``` 12 | 13 | **Next step:** [Enabling StrictPhp](bootstrap.md) 14 | -------------------------------------------------------------------------------- /tests/functional/int-typed-property-violation.phpt: -------------------------------------------------------------------------------- 1 | --TEST-- 2 | Verifies that writing a non-integer to an integer-typed property causes a fatal error 3 | --FILE-- 4 | property = '123'; 11 | ?> 12 | --EXPECTF-- 13 | %AFatal error: Uncaught exception 'ErrorException' with message 'NOPE'%a 14 | -------------------------------------------------------------------------------- /tests/functional/string-typed-property-violation.phpt: -------------------------------------------------------------------------------- 1 | --TEST-- 2 | Verifies that writing a non-string to an string-typed property causes a fatal error 3 | --FILE-- 4 | property = 123; 11 | ?> 12 | --EXPECTF-- 13 | %AFatal error: Uncaught exception 'ErrorException' with message 'NOPE'%a 14 | -------------------------------------------------------------------------------- /tests/functional/integer-typed-property-violation.phpt: -------------------------------------------------------------------------------- 1 | --TEST-- 2 | Verifies that writing a non-integer to an integer-typed property causes a fatal error 3 | --FILE-- 4 | property = '123'; 11 | ?> 12 | --EXPECTF-- 13 | %AFatal error: Uncaught exception 'ErrorException' with message 'NOPE'%a 14 | -------------------------------------------------------------------------------- /tests/functional/object-typed-property-violation.phpt: -------------------------------------------------------------------------------- 1 | --TEST-- 2 | Verifies that writing a non-object to an object-typed property causes a fatal error 3 | --FILE-- 4 | property = 'non-object'; 11 | ?> 12 | --EXPECTF-- 13 | %AFatal error: Uncaught exception 'ErrorException' with message 'NOPE'%a -------------------------------------------------------------------------------- /tests/functional/callable-typed-property-violation.phpt: -------------------------------------------------------------------------------- 1 | --TEST-- 2 | Verifies that writing a non-callable to an callable-typed property causes a fatal error 3 | --FILE-- 4 | property = 123; 11 | ?> 12 | --EXPECTF-- 13 | %ACatchable fatal error: Argument 1 passed to %a must be callable, integer given%a 14 | -------------------------------------------------------------------------------- /tests/functional/array-typed-property-violation.phpt: -------------------------------------------------------------------------------- 1 | --TEST-- 2 | Verifies that writing a non-array to an array-typed property causes a fatal error 3 | --FILE-- 4 | property = 'non-array'; 11 | ?> 12 | --EXPECTF-- 13 | %ACatchable fatal error: Argument 1 passed to %a must be of the type array, string given%a 14 | -------------------------------------------------------------------------------- /tests/functional/uninitialized-parent-class-properties-violation.phpt: -------------------------------------------------------------------------------- 1 | --TEST-- 2 | Verifies that an object that doesn't initialize all the properties in a parent class is considered as a failure 3 | --FILE-- 4 | 10 | --EXPECTF-- 11 | %ACatchable fatal error: Argument 1 passed to %a must be %a array, null given%a -------------------------------------------------------------------------------- /tests/functional/uninitialized-properties-in-constructor-violation.phpt: -------------------------------------------------------------------------------- 1 | --TEST-- 2 | Verifies that an object that doesn't initialize all its properties in a public constructor is considered as a failure 3 | --FILE-- 4 | 10 | --EXPECTF-- 11 | %ACatchable fatal error: Argument 1 passed to %a must be %a array, null given%a -------------------------------------------------------------------------------- /tests/functional/resource-typed-property-violation.phpt: -------------------------------------------------------------------------------- 1 | --TEST-- 2 | Verifies that writing a non-resource to an resource-typed property causes a fatal error 3 | --FILE-- 4 | property = 123; 11 | ?> 12 | --EXPECTF-- 13 | %AFatal error: Uncaught exception 'ErrorException' with message 'Unsupported type "object" given, expecting "resource"'%a 14 | -------------------------------------------------------------------------------- /tests/functional/return-string-type-violation.phpt: -------------------------------------------------------------------------------- 1 | --TEST-- 2 | Verifies return not-string should raise a fatal error 3 | --FILE-- 4 | expectString('yada yada'); 11 | 12 | echo "OK1\n"; 13 | 14 | $object->expectString(1e4); 15 | 16 | echo "OK2\n"; 17 | ?> 18 | --EXPECTF-- 19 | OK1 20 | 21 | %AFatal error: Uncaught exception 'ErrorException' with message 'NOPE'%a 22 | -------------------------------------------------------------------------------- /tests/functional/multiple-parameter-types-resolver.phpt: -------------------------------------------------------------------------------- 1 | --TEST-- 2 | Verifies that pass different deep inside collection parameter to a method call raises an Exception 3 | --FILE-- 4 | foo([['string']]); 11 | 12 | echo "OK1!\n"; 13 | 14 | $object->foo([['123', '456', '789']]); 15 | 16 | echo "OK2!\n"; 17 | 18 | $object->foo([true]); 19 | 20 | echo "OK3!\n"; 21 | 22 | ?> 23 | --EXPECTF-- 24 | OK1! 25 | OK2! 26 | OK3! 27 | -------------------------------------------------------------------------------- /phpunit.xml.dist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | ./tests/functional 5 | 6 | 7 | tests/StrictPhpTest 8 | 9 | 10 | 11 | ./src 12 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /tests/functional/stdclass-typed-property-violation.phpt: -------------------------------------------------------------------------------- 1 | --TEST-- 2 | Verifies that writing a wrong object type to an strict object typed property causes a fatal error 3 | --FILE-- 4 | property = new \StrictPhpTestAsset\ClassWithStdClassTypedProperty(); 11 | ?> 12 | --EXPECTF-- 13 | %ACatchable fatal error: Argument 1 passed to%a must be an instance of stdClass, instance of StrictPhpTestAsset\ClassWithStdClassTypedProperty given%a 14 | -------------------------------------------------------------------------------- /docs/current-limitation.md: -------------------------------------------------------------------------------- 1 | --- 2 | currentMenu: current-limitation 3 | --- 4 | 5 | ## Current limitations 6 | 7 | This package uses [voodoo magic](http://ocramius.github.io/voodoo-php/) to 8 | operate, specifically [go-aop-php](https://github.com/lisachenko/go-aop-php). 9 | 10 | Go AOP PHP has some limitations when it comes to intercepting access to 11 | private class members, so please be aware that it has limited scope (for now). 12 | 13 | This package only works against autoloaded classes; classes that aren't handled by 14 | an autoloader cannot be rectified by StrictPhp. 15 | -------------------------------------------------------------------------------- /tests/functional/return-self-type-violation.phpt: -------------------------------------------------------------------------------- 1 | --TEST-- 2 | Verifies return not-self compatible should raise a fatal error 3 | --FILE-- 4 | expectSelf($object); 11 | 12 | echo "OK1\n"; 13 | 14 | $object->expectSelf(new SplStack()); 15 | 16 | echo "OK2\n"; 17 | ?> 18 | --EXPECTF-- 19 | OK1 20 | 21 | %Aatal error: Argument 1 passed to %aObjectTypeChecker%a must be an instance of StrictPhpTestAsset\ClassWithReturnTypeMethod%a instance of SplStack given%a 22 | -------------------------------------------------------------------------------- /tests/functional/return-this-type-violation.phpt: -------------------------------------------------------------------------------- 1 | --TEST-- 2 | Verifies return not-self compatible should raise a fatal error 3 | --FILE-- 4 | expectThis($object); 11 | 12 | echo "OK1\n"; 13 | 14 | $object->expectThis(new SplStack()); 15 | 16 | echo "OK2\n"; 17 | ?> 18 | --EXPECTF-- 19 | OK1 20 | 21 | %Aatal error: Argument 1 passed to %aObjectTypeChecker%a must be an instance of StrictPhpTestAsset\ClassWithReturnTypeMethod%a instance of SplStack given%a 22 | -------------------------------------------------------------------------------- /tests/functional/return-static-type-violation.phpt: -------------------------------------------------------------------------------- 1 | --TEST-- 2 | Verifies return not-static compatible should raise a fatal error 3 | --FILE-- 4 | expectStatic($object); 11 | 12 | echo "OK1\n"; 13 | 14 | $object->expectStatic(new SplStack()); 15 | 16 | echo "OK2\n"; 17 | ?> 18 | --EXPECTF-- 19 | OK1 20 | 21 | %Aatal error: Argument 1 passed to %aObjectTypeChecker::{closure}() must be an instance of %aClassWithReturnTypeMethod%a instance of SplStack given%a 22 | -------------------------------------------------------------------------------- /tests/functional/return-object-type-violation.phpt: -------------------------------------------------------------------------------- 1 | --TEST-- 2 | Verifies return not-object should raise a fatal error 3 | --FILE-- 4 | expectObject(new \StdClass); 11 | 12 | echo "OK1\n"; 13 | 14 | $object->expectObject(new \SplStack()); 15 | 16 | echo "OK2\n"; 17 | 18 | $object->expectObject('non-object'); 19 | 20 | echo "OK3\n"; 21 | 22 | ?> 23 | --EXPECTF-- 24 | OK1 25 | OK2 26 | 27 | %AFatal error: Uncaught exception 'ErrorException' with message 'NOPE'%a 28 | -------------------------------------------------------------------------------- /tests/functional/self-typed-property-violation.phpt: -------------------------------------------------------------------------------- 1 | --TEST-- 2 | Verifies that writing a non self type object to an type hinted property causes a fatal error 3 | --FILE-- 4 | property = new \StrictPhpTestAsset\ClassWithSelfTypedProperty(); 11 | 12 | echo "OK\n"; 13 | 14 | $object->property = new stdClass(); 15 | 16 | echo 'Never reached!'; 17 | ?> 18 | --EXPECTF-- 19 | OK 20 | 21 | %ACatchable fatal error:%amust be an instance of %a, instance of stdClass given,%a 22 | -------------------------------------------------------------------------------- /tests/functional/return-stdclass-type-violation.phpt: -------------------------------------------------------------------------------- 1 | --TEST-- 2 | Verifies return not-string should raise a fatal error 3 | --FILE-- 4 | expectStdClass(new \StdClass); 11 | 12 | echo "OK1\n"; 13 | 14 | $object->expectStdClass(new SplStack()); 15 | 16 | echo "OK2\n"; 17 | ?> 18 | --EXPECTF-- 19 | OK1 20 | 21 | %Aatal error: Argument 1 passed to StrictPhp\TypeChecker\TypeChecker\ObjectTypeChecker::{closure}() must be an instance of stdClass, instance of SplStack given%a 22 | -------------------------------------------------------------------------------- /tests/functional/static-typed-property-violation.phpt: -------------------------------------------------------------------------------- 1 | --TEST-- 2 | Verifies that writing a non static type object to an type hinted property causes a fatal error 3 | --FILE-- 4 | property = new \StrictPhpTestAsset\ClassWithStaticTypedProperty(); 11 | 12 | echo "OK\n"; 13 | 14 | $object->property = new stdClass(); 15 | 16 | echo 'Never reached!'; 17 | ?> 18 | --EXPECTF-- 19 | OK 20 | 21 | %ACatchable fatal error:%amust be an instance of %a, instance of stdClass given,%a 22 | -------------------------------------------------------------------------------- /tests/functional/immutable-property-violation.phpt: -------------------------------------------------------------------------------- 1 | --TEST-- 2 | Verifies that overwrite a property value on a immutable typed property causes a fatal error 3 | --FILE-- 4 | property = 'stuff'; 11 | 12 | echo 'OK'; 13 | 14 | $object->property = 'overwrite (not possible)' 15 | ?> 16 | --EXPECTF-- 17 | OK 18 | %AFatal error: Uncaught exception 'RuntimeException' with message 'Trying to overwrite property %a#$property of object %a#%a with a value of type "string". The property was already given a value of type string%a 19 | -------------------------------------------------------------------------------- /tests/functional/same-typed-property-violation.phpt: -------------------------------------------------------------------------------- 1 | --TEST-- 2 | Verifies that writing a non self compatible object to an type hinted property causes a fatal error 3 | --FILE-- 4 | property = new \StrictPhpTestAsset\ClassWithSameTypedProperty(); 11 | 12 | echo "OK\n"; 13 | 14 | $object->property = new stdClass(); 15 | 16 | echo 'Never reached!'; 17 | ?> 18 | --EXPECTF-- 19 | OK 20 | 21 | %ACatchable fatal error: Argument 1 passed to %a must be an instance of StrictPhpTestAsset\ClassWithSameTypedProperty, instance of stdClass given%a 22 | -------------------------------------------------------------------------------- /tests/functional/return-mixed-collection-type-violation.phpt: -------------------------------------------------------------------------------- 1 | --TEST-- 2 | Verifies return not-boolean or string collection should raise a fatal error 3 | --FILE-- 4 | expectMixedDataCollection([['yada', 'yada']]); 11 | 12 | echo "OK1\n"; 13 | 14 | $object->expectMixedDataCollection([true, false]); 15 | 16 | echo "OK2\n"; 17 | 18 | $object->expectMixedDataCollection([[true], ['string'], true]); 19 | 20 | echo "OK3\n"; 21 | ?> 22 | --EXPECTF-- 23 | OK1 24 | OK2 25 | 26 | %AFatal error: Uncaught exception 'ErrorException' with message 'NOPE'%a 27 | -------------------------------------------------------------------------------- /docs/parameter-checking.md: -------------------------------------------------------------------------------- 1 | --- 2 | currentMenu: parameter-checking 3 | --- 4 | 5 | # Parameter checking 6 | 7 | Enabled via flag `StrictPhp\StrictPhpKernel::CHECK_PUBLIC_METHOD_PARAMETER_TYPE`. 8 | 9 | StrictPhp also provides a way to check parameters types in more detail during 10 | public method calls. 11 | 12 | Specifically, the following code will work in PHP: 13 | 14 | ```php 15 | final class Invoice 16 | { 17 | /** 18 | * @param LineItem[] $lineItems 19 | */ 20 | public function __construct(array $lineItems) 21 | { 22 | // ... 23 | } 24 | } 25 | 26 | $invoice = new Invoice(['foo', 'bar']); 27 | ``` 28 | 29 | This code will crash in StrictPhp due to the type mismatch in `$lineItems` (which 30 | should be a collection of `LineItem` objects instead). 31 | 32 | **Next step:** [Current limitations](current-limitation.md) 33 | -------------------------------------------------------------------------------- /tests/functional/call-to-non-interfaced-method-violation.phpt: -------------------------------------------------------------------------------- 1 | --TEST-- 2 | Verifies that calling a non-interfaced method in a context that is only aware of the interface causes a fatal error 3 | --FILE-- 4 | sayHello($helloSayer, 'Marco'); 12 | 13 | echo "\nOK"; 14 | 15 | $helloUser->doSomethingElseWithHello($helloSayer); 16 | 17 | echo 'Never reached' 18 | 19 | ?> 20 | --EXPECTF-- 21 | %AFatal error: Uncaught exception 'InterNations\Component\TypeJail\Exception\JailException' with message 'Jailed method "%a::otherMethod()" invoked on proxy restricted to "StrictPhpTestAsset\HelloInterface". Check file "%a" to find out which method calls are allowed%a -------------------------------------------------------------------------------- /docs/bootstrap.md: -------------------------------------------------------------------------------- 1 | --- 2 | currentMenu: bootstrap 3 | --- 4 | 5 | # Enabling StrictPhp 6 | 7 | To bootstrap `StrictPhp`, we initialize the `StrictPhp\StrictPhpKernel` singleton: 8 | 9 | ```php 10 | \StrictPhp\StrictPhpKernel::getInstance()->init([ 11 | 'debug' => true, // Use 'false' for production mode 12 | 'cacheDir' => sys_get_temp_dir(), // Adjust this path if needed 13 | 'includePaths' => [ 14 | __DIR__ . '/path/to/your/sources', // Include paths restricts the directories 15 | // where aspects should be applied, or empty 16 | // for all source files 17 | ], 18 | ]); 19 | ``` 20 | 21 | More supported configuration keys can be found in the [Go! AOP PHP documentation](http://go.aopphp.com/docs/initial-configuration) 22 | 23 | **Next step:** [Basic strict property typing](basic-strict.md) 24 | -------------------------------------------------------------------------------- /docs/immutability.md: -------------------------------------------------------------------------------- 1 | --- 2 | currentMenu: immutable 3 | --- 4 | 5 | # Immutability 6 | 7 | We also provided a `@immutable` annotation, which works pretty much as constant, ideal to use on values objects, not mutable. 8 | 9 | A property marked as immutable can receive a value only a single time. The value can never be overwritten. 10 | 11 | A simple value object can be expressed using `StrictPhp` as the follow: 12 | 13 | ```php 14 | class Sushi 15 | { 16 | /** 17 | * @immutable 18 | */ 19 | public $price; 20 | } 21 | ``` 22 | 23 | If the value of `Sushi::$price` is gonna be changed, we will get an exception. 24 | 25 | ```php 26 | $sushi = new Sushi; 27 | $sushi->price = 2.0; 28 | 29 | // Okay! 30 | 31 | $sushi->price = 1.9; // Exception is raised 32 | 33 | ``` 34 | 35 | **Next step:** [Public constructor property initialization checks](public-constructor-property-initialization-checks.md) 36 | -------------------------------------------------------------------------------- /docs/return-type-checks.md: -------------------------------------------------------------------------------- 1 | --- 2 | currentMenu: return-type-checks 3 | --- 4 | 5 | # Return type checks 6 | 7 | Quite similar to the above functionality, this feature will prevent your application 8 | from **returning** illegal values from methods that are type-hinted (via docblock) 9 | differently. As an example, consider following method: 10 | 11 | ```php 12 | class Example 13 | { 14 | /** 15 | * @return string 16 | */ 17 | public function dummyReturn($value) 18 | { 19 | return $value; 20 | } 21 | } 22 | ``` 23 | 24 | Following code will work: 25 | 26 | ```php 27 | (new Example())->dummyReturn('string'); 28 | ``` 29 | 30 | Following code will crash: 31 | 32 | ```php 33 | (new Example())->dummyReturn(123); 34 | ``` 35 | 36 | Please note that this kind of feature currently only works with public and 37 | protected methods. 38 | 39 | **Next step:** [Immutability](immutability.md) 40 | -------------------------------------------------------------------------------- /tests/functional/typed-array-property-violation.phpt: -------------------------------------------------------------------------------- 1 | --TEST-- 2 | Verifies that writing a non-array equivalent to an array-typed property causes a fatal error 3 | --FILE-- 4 | property = []; 11 | 12 | echo "OK1\n"; 13 | 14 | $object->property = new ArrayObject([]); 15 | 16 | echo "OK2\n"; 17 | 18 | $object->property = [new stdClass(), new stdClass()]; 19 | 20 | echo "OK3\n"; 21 | 22 | $object->property = new ArrayObject([new stdClass(), new stdClass()]); 23 | 24 | echo "OK4\n"; 25 | 26 | $object->property = [new stdClass(), new \StrictPhpTestAsset\ClassWithTypedArrayProperty()]; 27 | ?> 28 | --EXPECTF-- 29 | OK1 30 | OK2 31 | OK3 32 | OK4 33 | 34 | %ACatchable fatal error: Argument 1 passed to %a must be an instance of stdClass, instance of StrictPhpTestAsset\ClassWithTypedArrayProperty given%a 35 | -------------------------------------------------------------------------------- /docs/property-type-checks.md: -------------------------------------------------------------------------------- 1 | --- 2 | currentMenu: property-type-checks 3 | --- 4 | 5 | # Per-property type checks 6 | 7 | Enabled via flag `StrictPhp\StrictPhpKernel::CHECK_PROPERTY_WRITE_TYPE`. 8 | 9 | This feature will prevent your application from assigning illegal values to 10 | properties that are type-hinted (via docblock) differently. As an example, 11 | consider following class: 12 | 13 | ```php 14 | class Example 15 | { 16 | /** 17 | * @var int|null 18 | */ 19 | public $integer; 20 | } 21 | ``` 22 | 23 | Following code will work: 24 | 25 | ```php 26 | $object = new Example(); 27 | 28 | $object->integer = 123; 29 | ``` 30 | 31 | Following code will crash: 32 | 33 | ```php 34 | $object = new Example(); 35 | 36 | $object->integer = '123'; 37 | ``` 38 | 39 | Please note that this kind of feature currently only works with public and 40 | protected properties. 41 | 42 | **Next step:** [Return type checks](return-type-checks.md) 43 | -------------------------------------------------------------------------------- /tests/functional/init.php: -------------------------------------------------------------------------------- 1 | true, 13 | 'cacheDir' => realpath(__DIR__ . '/..') . '/integration-tests-go-cache/', 14 | 'includePaths' => [ 15 | realpath(__DIR__ . '/../StrictPhpTestAsset'), 16 | ], 17 | ], 18 | [ 19 | StrictPhpKernel::CHECK_STATE_AFTER_CONSTRUCTOR_CALL, 20 | StrictPhpKernel::JAIL_PUBLIC_METHOD_PARAMETERS, 21 | StrictPhpKernel::CHECK_STATE_AFTER_PUBLIC_METHOD_CALL, 22 | StrictPhpKernel::CHECK_PUBLIC_METHOD_PARAMETER_TYPE, 23 | StrictPhpKernel::CHECK_PUBLIC_METHOD_RETURN_TYPE, 24 | StrictPhpKernel::CHECK_PROPERTY_WRITE_IMMUTABILITY, 25 | StrictPhpKernel::CHECK_PROPERTY_WRITE_TYPE, 26 | ] 27 | ); 28 | -------------------------------------------------------------------------------- /tests/functional/call-to-method-with-typed-array-violation.phpt: -------------------------------------------------------------------------------- 1 | --TEST-- 2 | Verifies that calling a method with a typed array parameter annotation causes a fatal error 3 | --FILE-- 4 | method([]); 10 | 11 | echo "OK\n"; 12 | 13 | (new \StrictPhpTestAsset\ClassWithTypedArrayMethodParameterAnnotation()) 14 | ->method([1, 2, 3]); 15 | 16 | echo "OK\n"; 17 | 18 | (new \StrictPhpTestAsset\ClassWithTypedArrayMethodParameterAnnotation()) 19 | ->method([new stdClass(), new stdClass(), new stdClass()]); 20 | 21 | echo "OK\n"; 22 | 23 | (new \StrictPhpTestAsset\ClassWithTypedArrayMethodParameterAnnotation()) 24 | ->method([new \StrictPhpTestAsset\ClassWithTypedArrayMethodParameterAnnotation()]); 25 | 26 | echo "Never reached\n"; 27 | 28 | ?> 29 | --EXPECTF-- 30 | OK 31 | OK 32 | OK 33 | %Aatal error: Argument 1 passed to %s must be an instance of stdClass, instance of %a given%a -------------------------------------------------------------------------------- /tests/functional/call-to-method-typed-with-multiparams-violation.phpt: -------------------------------------------------------------------------------- 1 | --TEST-- 2 | Verifies that calling a method with a typed multiple array parameter annotation causes a fatal error 3 | --FILE-- 4 | method([], [['foo']], true); 10 | 11 | echo "OK\n"; 12 | 13 | (new \StrictPhpTestAsset\ClassWithMultipleParamsTypedMethodAnnotation()) 14 | ->method([1, 2, 3], [['bar']], false); 15 | 16 | echo "OK\n"; 17 | 18 | (new \StrictPhpTestAsset\ClassWithMultipleParamsTypedMethodAnnotation()) 19 | ->method([new stdClass(), new stdClass(), new stdClass()], [['foo']], true); 20 | 21 | echo "OK\n"; 22 | 23 | (new \StrictPhpTestAsset\ClassWithMultipleParamsTypedMethodAnnotation()) 24 | ->method([], true, false); 25 | 26 | echo "Never reached\n"; 27 | 28 | ?> 29 | --EXPECTF-- 30 | OK 31 | OK 32 | OK 33 | %Aatal error: Argument 2 passed to StrictPhpTestAsset\ClassWithMultipleParamsTypedMethodAnnotation::method() must be of the type array, boolean given%a -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2015 Roave, LLC. 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy of 4 | this software and associated documentation files (the "Software"), to deal in 5 | the Software without restriction, including without limitation the rights to 6 | use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies 7 | of the Software, and to permit persons to whom the Software is furnished to do 8 | so, subject to the following conditions: 9 | 10 | The above copyright notice and this permission notice shall be included in all 11 | copies or substantial portions of the Software. 12 | 13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 19 | SOFTWARE. 20 | -------------------------------------------------------------------------------- /tests/StrictPhpTestAsset/ClassWithGenericNonTypedProperty.php: -------------------------------------------------------------------------------- 1 | arrayProperty = ['initial status']; 43 | } 44 | } 45 | ``` 46 | 47 | **Next step:** [Parameter interface jailing](parameter-interface-jailing.md) 48 | -------------------------------------------------------------------------------- /tests/StrictPhpTestAsset/ClassWithSelfTypedProperty.php: -------------------------------------------------------------------------------- 1 | sadTrombone(); 33 | 34 | return; 35 | } 36 | 37 | // interface respected 38 | $horn->honk(); 39 | } 40 | } 41 | ``` 42 | 43 | ```php 44 | $car = new Car(); 45 | $horn = new TheUsualHorn(); 46 | 47 | $car->honk($horn, false); // works 48 | $car->honk($horn, true); // crashes 49 | ``` 50 | 51 | This prevents consumers of your APIs to design their code against non-API methods. 52 | 53 | **Next step:** [Parameter checking](parameter-checking.md) 54 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: php 2 | 3 | php: 4 | - 5.6 5 | - 7 6 | - hhvm 7 | 8 | env: 9 | global: 10 | - GIT_NAME: "'Marco Pivetta'" 11 | - GIT_EMAIL: ocramius@gmail.com 12 | - GH_REF: github.com/Roave/StrictPhp 13 | - secure: Zcnyy45DPmxCq5s3mJLAM07WC09cZLNioYBUtuN89wnTqUW+28BnU6BMIUlHSNIUA1j0TvvtCeI/E8mI9HHRcWy4tsH2hm2JnCJay0hR1CZegsP34xuieBR6KB8Uw6effZzcOVqbZAJacW0bI815rdVpRIEPDTgmucNCOLH8QYgtTzad720XPLoJOg8JT9ydcmR6sY3p096gxvaoNl3rJyXMdgp5zuf/AqkaxLLvLepGmszypkmXgVxHYds369iS26F6pA2sdqIEDuJjhVreoYyY1sJA9Eisqg0WTLkuClmfJ8t47WGZxWgkgPkz9hXYsnrdoG9NbyVZsKurgvAHDVsSnFBXzg3MIBsuVS8x6SHgsf35AI0RzH29n03k/eQGFY9fz6q1BB10NFYhpgp2n1IA4clyf91ozyXxDWF15LgGNdYjKOeD/9AMqr9RH6Z7pBWihTWOa1qg7bxzgeC6yuvhvawQNJq0nYW2FhvXK6vI5fjRqfRfD9xUzqcpo9MkrTSIxTC+LMUqq1vEczNQyq1p/jPOhRURCDLdMXg4Tmm7oolS41CC4GHeODO1JY0t9ZkIUd2H/pa3wPIrImDkm6ZiwXEsdKc0vNq1g0SyBpPpSn1rct5H6U6GPKHUUFBgAhzqDI23syuaqwUhToYZJZaZkMZ+7sm/pTs/hG+vqcc= 14 | 15 | before_script: 16 | - composer self-update 17 | - composer install --dev 18 | 19 | script: 20 | - ./vendor/bin/phpunit --disallow-test-output --coverage-clover ./clover.xml 21 | 22 | matrix: 23 | allow_failures: 24 | - php: hhvm 25 | - php: 7 26 | 27 | after_script: 28 | - if [ $TRAVIS_PHP_VERSION = '5.6' ]; then wget https://scrutinizer-ci.com/ocular.phar; php ocular.phar code-coverage:upload --format=php-clover clover.xml; fi 29 | - ./vendor/bin/couscous travis-auto-deploy --php-version=5.6 30 | -------------------------------------------------------------------------------- /tests/StrictPhpTestAsset/ParentClassWithInitializingConstructor.php: -------------------------------------------------------------------------------- 1 | property = ['the parent class array']; 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /tests/StrictPhpTestAsset/ClassWithMultipleParamsTypedMethodAnnotation.php: -------------------------------------------------------------------------------- 1 | property = ['the child class array']; 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /couscous.yml: -------------------------------------------------------------------------------- 1 | github: 2 | user: Roave 3 | repo: StrictPhp 4 | 5 | title: StrictPhp 6 | subTitle: An AOP-based strict type checks for PHP 7 | 8 | baseUrl: http://roave.github.io/StrictPhp 9 | 10 | exclude: 11 | - vendor 12 | - src 13 | - tests 14 | 15 | menu: 16 | items: 17 | home: 18 | text: Home page 19 | relativeUrl: index.html 20 | install: 21 | text: Installing 22 | relativeUrl: docs/installing.html 23 | bootstrap: 24 | text: Enabling 25 | relativeUrl: docs/bootstrap.html 26 | basic-usage: 27 | text: Basic usage 28 | relativeUrl: docs/basic-strict.html 29 | property-type-checks: 30 | text: Property Type checks 31 | relativeUrl: docs/property-type-checks.html 32 | return-type-checks: 33 | text: Return Type checks 34 | relativeUrl: docs/return-type-checks.html 35 | immutability: 36 | text: Immutability 37 | relativeUrl: docs/Immutability.html 38 | public-constructor-property-initialization-checks: 39 | text: Public constructor property initialization checks 40 | relativeUrl: docs/public-constructor-property-initialization-checks.html 41 | parameter-interface-jailing: 42 | text: Parameter interface jailing 43 | relativeUrl: docs/parameter-interface-jailing.html 44 | parameter-checking: 45 | text: Parameter checking 46 | relativeUrl: docs/parameter-checking.html 47 | current-limitation: 48 | text: Current limitation 49 | relativeUrl: docs/current-limitation.html 50 | -------------------------------------------------------------------------------- /src/StrictPhp/TypeChecker/TypeCheckerInterface.php: -------------------------------------------------------------------------------- 1 | hello($name); 32 | } 33 | 34 | /** 35 | * @param HelloInterface $helloSayer 36 | * 37 | * @return string 38 | */ 39 | public function doSomethingElseWithHello(HelloInterface $helloSayer) 40 | { 41 | /* @var $helloSayer ClassWithHelloImplementationAndAdditionalMethod */ 42 | return $helloSayer->otherMethod(); 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "roave/strict-php", 3 | "description": "A strict runtime type and invariant checker for developing safer PHP applications", 4 | "type": "library", 5 | "license": "MIT", 6 | "homepage": "https://github.com/Roave/StrictPhp", 7 | "keywords": [ 8 | "php", 9 | "strict", 10 | "type safety", 11 | "continuous integration", 12 | "analysis", 13 | "development" 14 | ], 15 | "authors": [ 16 | { 17 | "name": "Marco Pivetta", 18 | "email": "ocramius@gmail.com", 19 | "homepage": "http://ocramius.github.io/" 20 | }, 21 | { 22 | "name": "Jefersson Nathan", 23 | "email": "malukenho@phpse.net" 24 | } 25 | ], 26 | "minimum-stability": "dev", 27 | "require": { 28 | "php": "~5.6|~7.0", 29 | "goaop/framework": "~1.0.0@ALPHA", 30 | "phpdocumentor/reflection-docblock": "~2.0", 31 | "phpdocumentor/type-resolver": "^0.1.5", 32 | "internations/type-jail": "0.4.*" 33 | }, 34 | "require-dev": { 35 | "phpunit/phpunit": "~4.7", 36 | "couscous/couscous": "dev-master" 37 | }, 38 | "autoload": { 39 | "psr-4": { 40 | "StrictPhp\\": "src/StrictPhp" 41 | } 42 | }, 43 | "autoload-dev": { 44 | "psr-4": { 45 | "StrictPhpTest\\": "tests/StrictPhpTest", 46 | "StrictPhpTestAsset\\": "tests/StrictPhpTestAsset" 47 | } 48 | }, 49 | "extra": { 50 | "branch-alias": { 51 | "dev-master": "1.0.x-dev" 52 | } 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /docs/basic-strict.md: -------------------------------------------------------------------------------- 1 | --- 2 | currentMenu: basic-usage 3 | --- 4 | 5 | # Basic strict property typing 6 | 7 | With a configured bootstrap, we use annotations on the properties to make your app operate in strict mode. 8 | 9 | `StrictPhp` uses PhpDocumentor compatible annotations. 10 | 11 | Take a look at the following class. 12 | 13 | ```php 14 | class Sushi 15 | { 16 | /** 17 | * @var float 18 | */ 19 | public $price; 20 | } 21 | ``` 22 | 23 | In this way, `StrictPhp` knows that we can only set a **float** value for the `Sushi#price` property. 24 | 25 | Let's try setting a float value. 26 | 27 | ```php 28 | $sushi = new Sushi; 29 | $sushi->price = 2.0; 30 | ``` 31 | 32 | That works perfectly fine. 33 | 34 | Let's try assigning a wrong data type to the property. 35 | 36 | ```php 37 | $sushi = new Sushi; 38 | $sushi->price = '2'; 39 | ``` 40 | 41 | We will get an exception: 42 | 43 | ``` 44 | Fatal error: Uncaught exception 'ErrorException' ... 45 | ``` 46 | 47 | This is the most basic functionality of `StrictPhp` 48 | 49 | ## Working with collections 50 | 51 | `StrictPhp` provide a way to work strictly with a collection of a data type. 52 | 53 | We can have a collection of a type marking the property with something like `string[]`, also we can have more levels `string[][]` and soon. 54 | 55 | ```php 56 | /** 57 | * @var Invoice[] 58 | */ 59 | public $invoice; 60 | ``` 61 | 62 | This can receive *only* a collection of `Invoice` objects. If any element on the array assigned to `$invoice` is not an 63 | instance of `Invoice` we will get an `Exception`. 64 | 65 | ## Supported annotation types 66 | 67 | - null 68 | - int|integer 69 | - mixed 70 | - float 71 | - string 72 | - array 73 | - callable 74 | - object 75 | - self 76 | - static 77 | - *Class|Interfaces names* 78 | - `[]` - *collection* 79 | 80 | **Next step:** [Property type checks](property-type-checks.md) 81 | -------------------------------------------------------------------------------- /src/StrictPhp/TypeChecker/TypeChecker/IntegerTypeChecker.php: -------------------------------------------------------------------------------- 1 | validate($value, $type)) { 49 | // @TODO bump to PHP 7 and use strict scalar types + a closure. 50 | throw new \ErrorException('NOPE'); 51 | } 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /src/StrictPhp/TypeChecker/TypeChecker/StringTypeChecker.php: -------------------------------------------------------------------------------- 1 | validate($value, $type)) { 49 | // @TODO bump to PHP 7 and use strict scalar types + a closure. 50 | throw new \ErrorException('NOPE'); 51 | } 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /src/StrictPhp/Aspect/PrePublicMethodAspect.php: -------------------------------------------------------------------------------- 1 | interceptors = $interceptors; 38 | } 39 | 40 | /** 41 | * @Go\Before("execution(public **->*(*))") 42 | * 43 | * @param AbstractMethodInvocation $methodInvocation 44 | * 45 | * @return mixed 46 | */ 47 | public function prePublicMethod(AbstractMethodInvocation $methodInvocation) 48 | { 49 | foreach ($this->interceptors as $interceptor) { 50 | $interceptor($methodInvocation); 51 | } 52 | 53 | return $methodInvocation->proceed(); 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /src/StrictPhp/TypeChecker/TypeChecker/GenericObjectTypeChecker.php: -------------------------------------------------------------------------------- 1 | getFqsen(); 34 | } 35 | 36 | /** 37 | * {@inheritDoc} 38 | */ 39 | public function validate($value, Type $type) 40 | { 41 | return is_object($value); 42 | } 43 | 44 | /** 45 | * {@inheritDoc} 46 | */ 47 | public function simulateFailure($value, Type $type) 48 | { 49 | if (! $this->validate($value, $type)) { 50 | // @TODO bump to PHP 7 and use strict scalar types + a closure. 51 | throw new \ErrorException('NOPE'); 52 | } 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /src/StrictPhp/TypeChecker/TypeChecker/CallableTypeChecker.php: -------------------------------------------------------------------------------- 1 | propertyWriteCheckers = $propertyWriteCheckers; 38 | } 39 | 40 | /** 41 | * @Go\Before("access(public|protected **->*)") 42 | * 43 | * @param FieldAccess $access 44 | * 45 | * @return mixed 46 | */ 47 | public function beforePropertyAccess(FieldAccess $access) 48 | { 49 | if (FieldAccess::WRITE !== $access->getAccessType()) { 50 | return $access->proceed(); 51 | } 52 | 53 | foreach ($this->propertyWriteCheckers as $checker) { 54 | $checker($access); 55 | } 56 | 57 | return $access->proceed(); 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /src/StrictPhp/Aspect/PostPublicMethodAspect.php: -------------------------------------------------------------------------------- 1 | 27 | */ 28 | final class PostPublicMethodAspect implements Aspect 29 | { 30 | /** 31 | * @var callable[] 32 | */ 33 | private $interceptors; 34 | 35 | /** 36 | * @param callable ...$interceptors 37 | */ 38 | public function __construct(callable ...$interceptors) 39 | { 40 | $this->interceptors = $interceptors; 41 | } 42 | 43 | /** 44 | * @Go\After("execution(public **->*(*))") 45 | * 46 | * @param AbstractMethodInvocation $methodInvocation 47 | * 48 | * @return mixed 49 | */ 50 | public function postPublicMethod(AbstractMethodInvocation $methodInvocation) 51 | { 52 | $scope = get_class($methodInvocation->getThis()); 53 | 54 | foreach ($this->interceptors as $interceptor) { 55 | $interceptor($methodInvocation, $scope); 56 | } 57 | 58 | return $methodInvocation->proceed(); 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /src/StrictPhp/TypeChecker/TypeChecker/MixedTypeChecker.php: -------------------------------------------------------------------------------- 1 | validate($value, $type); 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /src/StrictPhp/Aspect/PostConstructAspect.php: -------------------------------------------------------------------------------- 1 | stateCheckers = $stateCheckers; 38 | } 39 | 40 | /** 41 | * @Go\After("execution(public **->__construct(*))") 42 | * 43 | * @param MethodInvocation $constructorInvocation 44 | * 45 | * @return mixed 46 | * 47 | * @throws \ErrorException|\Exception 48 | */ 49 | public function postConstruct(MethodInvocation $constructorInvocation) 50 | { 51 | $that = $constructorInvocation->getThis(); 52 | $scope = $constructorInvocation->getMethod()->getDeclaringClass()->getName(); 53 | 54 | array_map( 55 | function (callable $checker) use ($that, $scope) { 56 | $checker($that, $scope); 57 | }, 58 | $this->stateCheckers 59 | ); 60 | 61 | return $constructorInvocation->proceed(); 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /tests/StrictPhpTestAsset/ClassWithReturnTypeMethod.php: -------------------------------------------------------------------------------- 1 | validate($value, $type)) { 59 | // @TODO bump to PHP 7 and use strict scalar types + a closure. 60 | throw new \ErrorException('NOPE'); 61 | } 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /src/StrictPhp/TypeChecker/TypeChecker/NullTypeChecker.php: -------------------------------------------------------------------------------- 1 | validate($value, $type)) { 62 | // @TODO bump to PHP 7 and use strict scalar types + a closure. 63 | throw new \ErrorException('NOPE'); 64 | } 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /src/StrictPhp/TypeChecker/TypeChecker/ResourceTypeChecker.php: -------------------------------------------------------------------------------- 1 | validate($value, $type)) { 59 | throw new \ErrorException(sprintf( 60 | 'Unsupported type "%s" given, expecting "%s"', 61 | gettype($type), 62 | 'resource' 63 | )); 64 | } 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /tests/StrictPhpTest/Reflection/AllPropertiesTest.php: -------------------------------------------------------------------------------- 1 | 31 | * @license MIT 32 | * 33 | * @group Coverage 34 | * 35 | * @covers \StrictPhp\Reflection\AllProperties 36 | */ 37 | class AllPropertiesTest extends \PHPUnit_Framework_TestCase 38 | { 39 | /** 40 | * @dataProvider propertiesCount 41 | * 42 | * @param string $className 43 | * @param int $expectedCount 44 | */ 45 | public function testPropertiesCount($className, $expectedCount) 46 | { 47 | $this->assertCount($expectedCount, (new AllProperties())->__invoke(new ReflectionClass($className))); 48 | } 49 | 50 | /** 51 | * @return int[][]|string[][] 52 | */ 53 | public function propertiesCount() 54 | { 55 | return [ 56 | [ClassWithIncorrectlyInitializingConstructor::class, 1], 57 | [ParentClassWithInitializingConstructor::class, 1], 58 | [ClassWithIncorrectlyInitializedParentClassProperties::class, 2], 59 | ]; 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /src/StrictPhp/Reflection/AllProperties.php: -------------------------------------------------------------------------------- 1 | allHierarchyClasses($class)) 36 | ); 37 | } 38 | 39 | /** 40 | * @param ReflectionClass $class 41 | * 42 | * @return ReflectionProperty[] 43 | */ 44 | private function propertiesOfClass(ReflectionClass $class) 45 | { 46 | $className = $class->getName(); 47 | 48 | return array_values(array_filter( 49 | $class->getProperties(), 50 | function (ReflectionProperty $property) use ($className) { 51 | return $property->getDeclaringClass()->getName() === $className; 52 | } 53 | )); 54 | } 55 | 56 | /** 57 | * @param ReflectionClass $class 58 | * 59 | * @return ReflectionClass[] all the classes in the hierarchy, starting from the given one as leaf 60 | */ 61 | private function allHierarchyClasses(ReflectionClass $class) 62 | { 63 | return ($parent = $class->getParentClass()) 64 | ? array_merge([$class], $this->allHierarchyClasses($parent)) 65 | : [$class]; 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /src/StrictPhp/AccessChecker/ReturnTypeChecker.php: -------------------------------------------------------------------------------- 1 | 28 | */ 29 | final class ReturnTypeChecker 30 | { 31 | /** 32 | * @var callable 33 | */ 34 | private $applyTypeChecks; 35 | 36 | /** 37 | * @param callable $applyTypeChecks 38 | */ 39 | public function __construct(callable $applyTypeChecks) 40 | { 41 | $this->applyTypeChecks = $applyTypeChecks; 42 | } 43 | 44 | /** 45 | * @param MethodInvocation $methodInvocation 46 | * 47 | * @return void 48 | * 49 | * @throws \ErrorException 50 | */ 51 | public function __invoke(MethodInvocation $methodInvocation) 52 | { 53 | $reflectionMethod = $methodInvocation->getMethod(); 54 | $applyTypeChecks = $this->applyTypeChecks; 55 | 56 | $applyTypeChecks( 57 | $this->getReturnDocblockType( 58 | $methodInvocation->getMethod()->getDeclaringClass()->getName(), 59 | $reflectionMethod 60 | ), 61 | $methodInvocation->proceed() 62 | ); 63 | } 64 | 65 | /** 66 | * @param string $contextClass 67 | * @param ReflectionMethod $reflectionMethod 68 | * 69 | * @return Type[] 70 | */ 71 | private function getReturnDocblockType($contextClass, ReflectionMethod $reflectionMethod) 72 | { 73 | return (new ReturnTypeFinder()) 74 | ->__invoke($reflectionMethod, $contextClass); 75 | } 76 | } 77 | -------------------------------------------------------------------------------- /src/StrictPhp/AccessChecker/PropertyWriteImmutabilityChecker.php: -------------------------------------------------------------------------------- 1 | getThis()) { 36 | return; 37 | } 38 | 39 | if (FieldAccess::WRITE !== $access->getAccessType()) { 40 | return; 41 | } 42 | 43 | $field = $access->getField(); 44 | 45 | $field->setAccessible(true); 46 | 47 | // simplistic check - won't check for multiple assignments of "null" to a "null" valued field 48 | if (null === ($currentValue = $field->getValue($that))) { 49 | return; 50 | } 51 | 52 | if (! (new DocBlock($field))->getTagsByName('immutable')) { 53 | return; 54 | } 55 | 56 | $newValue = $access->getValueToSet(); 57 | 58 | throw new \RuntimeException(sprintf( 59 | 'Trying to overwrite property %s#$%s of object %s#%s with a value of type "%s".' 60 | . ' The property was already given a value of type %s', 61 | $field->getDeclaringClass()->getName(), 62 | $field->getName(), 63 | get_class($that), 64 | spl_object_hash($that), 65 | is_object($newValue) ? get_class($newValue) : gettype($newValue), 66 | is_object($currentValue) ? get_class($currentValue) : gettype($currentValue) 67 | )); 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /src/StrictPhp/TypeChecker/TypeChecker/ObjectTypeChecker.php: -------------------------------------------------------------------------------- 1 | getFqsen(); 34 | } 35 | 36 | /** 37 | * {@inheritDoc} 38 | * 39 | * @throws \InvalidArgumentException 40 | */ 41 | public function validate($value, Type $type) 42 | { 43 | if (! $type instanceof Object_) { 44 | throw new \InvalidArgumentException(sprintf( 45 | 'Non-object type "%s" given, expected "%s"', 46 | get_class($type), 47 | Object_::class 48 | )); 49 | } 50 | 51 | if (! $fqcn = $type->getFqsen()) { 52 | throw new \InvalidArgumentException(sprintf( 53 | 'The provided type of type "%s" does not have a FQCN', 54 | get_class($type) 55 | )); 56 | } 57 | 58 | $fqcnString = (string) $fqcn; 59 | 60 | return $value instanceof $fqcnString; 61 | } 62 | 63 | /** 64 | * {@inheritDoc} 65 | * 66 | * @throws \InvalidArgumentException 67 | */ 68 | public function simulateFailure($value, Type $type) 69 | { 70 | if (! $this->canApplyToType($type)) { 71 | throw new \InvalidArgumentException(sprintf( 72 | 'The provided type "%s" does not refer to a valid class', 73 | $type 74 | )); 75 | } 76 | 77 | /* @var $callback callable */ 78 | $callback = eval(sprintf('return function (%s $value) {};', $type)); 79 | 80 | $callback($value); 81 | } 82 | } 83 | -------------------------------------------------------------------------------- /src/StrictPhp/TypeChecker/ApplyTypeChecks.php: -------------------------------------------------------------------------------- 1 | typeCheckers = $typeCheckers; 36 | } 37 | 38 | /** 39 | * @param \phpDocumentor\Reflection\Type[] $allowedTypes 40 | * @param mixed $value 41 | * 42 | * @return void 43 | * 44 | * @throws \ErrorException|\Exception 45 | */ 46 | public function __invoke(array $allowedTypes, $value) 47 | { 48 | // @todo turn into functional? 49 | $validCheckers = []; 50 | 51 | foreach ($allowedTypes as $type) { 52 | foreach ($this->typeCheckers as $typeChecker) { 53 | if ($typeChecker->canApplyToType($type)) { 54 | $validCheckers[] = [$typeChecker, $type]; 55 | } 56 | } 57 | } 58 | 59 | $applicableCheckers = array_filter( 60 | $validCheckers, 61 | function (array $typeCheckerData) use ($value) { 62 | /* @var $checker TypeCheckerInterface */ 63 | /* @var $type Type */ 64 | list($checker, $type) = $typeCheckerData; 65 | 66 | return $checker->validate($value, $type); 67 | } 68 | ); 69 | 70 | array_map( 71 | function (array $typeCheckerData) use ($value) { 72 | /* @var $checker TypeCheckerInterface */ 73 | /* @var $type Type */ 74 | list($checker, $type) = $typeCheckerData; 75 | 76 | $checker->simulateFailure($value, $type); 77 | }, 78 | $applicableCheckers ?: $validCheckers 79 | ); 80 | } 81 | } 82 | -------------------------------------------------------------------------------- /src/StrictPhp/AccessChecker/PropertyWriteTypeChecker.php: -------------------------------------------------------------------------------- 1 | getAccessType()) { 46 | return; 47 | } 48 | 49 | $that = $access->getThis(); 50 | $contextClass = $that ? get_class($that) : $access->getField()->getDeclaringClass()->getName(); 51 | 52 | $baseCheckers = [ 53 | new IntegerTypeChecker(), 54 | new CallableTypeChecker(), 55 | new StringTypeChecker(), 56 | new GenericObjectTypeChecker(), 57 | new ObjectTypeChecker(), 58 | new ResourceTypeChecker(), 59 | new MixedTypeChecker(), 60 | new NullTypeChecker(), 61 | ]; 62 | 63 | (new ApplyTypeChecks( 64 | new TypedTraversableChecker(...$baseCheckers), 65 | ...$baseCheckers 66 | ))->__invoke( 67 | (new PropertyTypeFinder())->__invoke($access->getField(), $contextClass), 68 | $access->getValueToSet() 69 | ); 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /tests/StrictPhpTestAsset/ClassWithVariadicInterfaceParameters.php: -------------------------------------------------------------------------------- 1 | applyTypeChecks = $applyTypeChecks; 44 | $this->findTypes = $findTypes; 45 | } 46 | 47 | /** 48 | * @param object $object 49 | * @param string $scope scope of the state checks 50 | * 51 | * @return void 52 | * 53 | * @throws \InvalidArgumentException 54 | * @throws \Exception 55 | * @throws \ErrorException 56 | */ 57 | public function __invoke($object, $scope) 58 | { 59 | if (! is_object($object)) { 60 | throw new \InvalidArgumentException(sprintf( 61 | 'Provided argument must be an object, %s given', 62 | gettype($object) 63 | )); 64 | } 65 | 66 | array_map( 67 | function (ReflectionProperty $property) use ($object) { 68 | $property->setAccessible(true); 69 | 70 | $this->checkProperty($property, $property->getValue($object)); 71 | }, 72 | (new AllProperties())->__invoke(new ReflectionClass($scope)) 73 | ); 74 | } 75 | 76 | /** 77 | * @param ReflectionProperty $property 78 | * @param mixed $value 79 | * 80 | * @return void 81 | * 82 | * @throws \Exception|\ErrorException 83 | */ 84 | private function checkProperty( 85 | ReflectionProperty $property, 86 | $value 87 | ) { 88 | $typeChecker = $this->applyTypeChecks; 89 | $findTypes = $this->findTypes; 90 | 91 | $typeChecker($findTypes($property, $property->getDeclaringClass()->getName()), $value); 92 | } 93 | } 94 | -------------------------------------------------------------------------------- /tests/StrictPhpTest/TypeChecker/ApplyTypeChecksTest.php: -------------------------------------------------------------------------------- 1 | 29 | * @license MIT 30 | * 31 | * @group Coverage 32 | * 33 | * @covers \StrictPhp\TypeChecker\ApplyTypeChecks 34 | */ 35 | class ApplyTypeChecksTest extends \PHPUnit_Framework_TestCase 36 | { 37 | public function testApplyCheckerProperly() 38 | { 39 | $booleanType = new Boolean(); 40 | $typeChecker = $this->getMock(TypeCheckerInterface::class); 41 | $typeChecker->expects($this->once()) 42 | ->method('canApplyToType') 43 | ->with($booleanType) 44 | ->will($this->returnValue(true)); 45 | 46 | $typeChecker->expects($this->once()) 47 | ->method('validate') 48 | ->will($this->returnValue(true)); 49 | 50 | $typeChecker->expects($this->once()) 51 | ->method('simulateFailure') 52 | ->will($this->returnValue(true)); 53 | 54 | $applyChecks = new ApplyTypeChecks($typeChecker); 55 | $applyChecks->__invoke([$booleanType], []); 56 | } 57 | 58 | public function testWillApplyNonFittingCheckersIfAnyAreFound() 59 | { 60 | $booleanType = new Boolean(); 61 | $typeChecker = $this->getMock(TypeCheckerInterface::class); 62 | $typeChecker->expects($this->once()) 63 | ->method('canApplyToType') 64 | ->with($booleanType) 65 | ->will($this->returnValue(true)); 66 | 67 | $typeChecker->expects($this->once()) 68 | ->method('validate') 69 | ->with([], $booleanType) 70 | ->will($this->returnValue(false)); 71 | 72 | $typeChecker->expects($this->once()) 73 | ->method('simulateFailure') 74 | ->with([], $booleanType) 75 | ->will($this->returnValue(true)); 76 | 77 | $applyChecks = new ApplyTypeChecks($typeChecker); 78 | $applyChecks->__invoke([$booleanType], []); 79 | } 80 | } 81 | -------------------------------------------------------------------------------- /tests/StrictPhpTest/Aspect/PrePublicMethodAspectTest.php: -------------------------------------------------------------------------------- 1 | 29 | * @license MIT 30 | * 31 | * @group Coverage 32 | * 33 | * @covers \StrictPhp\Aspect\PrePublicMethodAspect 34 | */ 35 | class PrePublicMethodAspectTest extends \PHPUnit_Framework_TestCase 36 | { 37 | /** 38 | * @var MethodInvocation|\PHPUnit_Framework_MockObject_MockObject 39 | */ 40 | private $methodInvocation; 41 | 42 | /** 43 | * @var \PHPUnit_Framework_MockObject_MockObject[]|callable[] 44 | */ 45 | private $callables = []; 46 | 47 | /** 48 | * {@inheritDoc} 49 | */ 50 | protected function setUp() 51 | { 52 | $this->methodInvocation = $this->getMock(MethodInvocation::class); 53 | $this->callables = [ 54 | $this->getMock('stdClass', ['__invoke']), 55 | $this->getMock('stdClass', ['__invoke']), 56 | ]; 57 | } 58 | 59 | public function testWillExecuteAllInterceptorsOnCall() 60 | { 61 | /* @var $methodInvocation AbstractMethodInvocation|\PHPUnit_Framework_MockObject_MockObject */ 62 | $methodInvocation = $this->getMockForAbstractClass(AbstractMethodInvocation::class, [], '', false); 63 | 64 | /* @var $callables callable[]|\PHPUnit_Framework_MockObject_MockObject[] */ 65 | $callables = [ 66 | $this->getMock('stdClass', ['__invoke']), 67 | $this->getMock('stdClass', ['__invoke']), 68 | $this->getMock('stdClass', ['__invoke']), 69 | ]; 70 | 71 | foreach ($callables as $callable) { 72 | $callable->expects($this->once())->method('__invoke')->with($methodInvocation); 73 | } 74 | 75 | $aspect = new PrePublicMethodAspect(...$callables); 76 | 77 | $methodInvocation->expects($this->once())->method('proceed')->will($this->returnValue('result')); 78 | 79 | $this->assertSame('result', $aspect->prePublicMethod($methodInvocation)); 80 | } 81 | } 82 | -------------------------------------------------------------------------------- /tests/StrictPhpTest/Aspect/PostPublicMethodAspectTest.php: -------------------------------------------------------------------------------- 1 | 29 | * @license MIT 30 | * 31 | * @group Coverage 32 | * 33 | * @covers \StrictPhp\Aspect\PostPublicMethodAspect 34 | */ 35 | class PostPublicMethodAspectTest extends \PHPUnit_Framework_TestCase 36 | { 37 | /** 38 | * @var MethodInvocation|\PHPUnit_Framework_MockObject_MockObject 39 | */ 40 | private $methodInvocation; 41 | 42 | /** 43 | * @var \PHPUnit_Framework_MockObject_MockObject[]|callable[] 44 | */ 45 | private $callables = []; 46 | 47 | /** 48 | * {@inheritDoc} 49 | */ 50 | protected function setUp() 51 | { 52 | $this->methodInvocation = $this->getMock(MethodInvocation::class); 53 | $this->callables = [ 54 | $this->getMock('stdClass', ['__invoke']), 55 | $this->getMock('stdClass', ['__invoke']), 56 | ]; 57 | } 58 | 59 | public function testWillExecuteAllInterceptorsOnCall() 60 | { 61 | /* @var $methodInvocation AbstractMethodInvocation|\PHPUnit_Framework_MockObject_MockObject */ 62 | $methodInvocation = $this->getMockForAbstractClass(AbstractMethodInvocation::class, [], '', false); 63 | 64 | /* @var $callables callable[]|\PHPUnit_Framework_MockObject_MockObject[] */ 65 | $callables = [ 66 | $this->getMock('stdClass', ['__invoke']), 67 | $this->getMock('stdClass', ['__invoke']), 68 | $this->getMock('stdClass', ['__invoke']), 69 | ]; 70 | 71 | foreach ($callables as $callable) { 72 | $callable->expects($this->once())->method('__invoke')->with($methodInvocation); 73 | } 74 | 75 | $aspect = new PostPublicMethodAspect(...$callables); 76 | 77 | $methodInvocation->expects($this->once())->method('proceed')->will($this->returnValue('result')); 78 | 79 | $this->assertSame('result', $aspect->postPublicMethod($methodInvocation)); 80 | } 81 | } 82 | -------------------------------------------------------------------------------- /src/StrictPhp/AccessChecker/ParameterTypeChecker.php: -------------------------------------------------------------------------------- 1 | applyTypeChecks = $applyTypeChecks; 38 | } 39 | 40 | /** 41 | * @param MethodInvocation $methodInvocation 42 | * 43 | * @return void 44 | * 45 | * @throws \ErrorException 46 | */ 47 | public function __invoke(MethodInvocation $methodInvocation) 48 | { 49 | $reflectionParameters = $methodInvocation->getMethod()->getParameters(); 50 | $applyTypeChecks = $this->applyTypeChecks; 51 | 52 | foreach ($methodInvocation->getArguments() as $argumentIndex => $argument) { 53 | $applyTypeChecks( 54 | $this->getParameterDocblockType( 55 | get_class($methodInvocation->getThis()), 56 | $reflectionParameters, 57 | $argumentIndex 58 | ), 59 | $argument 60 | ); 61 | } 62 | } 63 | 64 | /** 65 | * @param string $contextClass 66 | * @param \ReflectionParameter[] $reflectionParameters 67 | * @param int $index 68 | * 69 | * @return Type[] 70 | */ 71 | private function getParameterDocblockType($contextClass, array $reflectionParameters, $index) 72 | { 73 | if (! isset($reflectionParameters[$index])) { 74 | /* @var $lastParameter \ReflectionParameter|bool */ 75 | if (($lastParameter = end($reflectionParameters)) && $lastParameter->isVariadic()) { 76 | return (new ParameterTypeFinder()) 77 | ->__invoke($lastParameter, $contextClass); 78 | } 79 | 80 | return []; 81 | } 82 | 83 | return (new ParameterTypeFinder()) 84 | ->__invoke($reflectionParameters[$index], $contextClass); 85 | } 86 | } 87 | -------------------------------------------------------------------------------- /tests/StrictPhpTest/AccessChecker/PropertyWriteTypeCheckerTest.php: -------------------------------------------------------------------------------- 1 | 30 | * @license MIT 31 | * 32 | * @group Coverage 33 | * 34 | * @covers \StrictPhp\AccessChecker\PropertyWriteTypeChecker 35 | */ 36 | class PropertyWriteTypeCheckerTest extends \PHPUnit_Framework_TestCase 37 | { 38 | /** 39 | * @var FieldAccess|\PHPUnit_Framework_MockObject_MockObject 40 | */ 41 | private $fieldAccess; 42 | 43 | /** 44 | * {@inheritDoc} 45 | */ 46 | protected function setUp() 47 | { 48 | $this->fieldAccess = $this->getMock(FieldAccess::class); 49 | 50 | $this->fieldAccess->expects($this->never())->method('proceed'); 51 | } 52 | 53 | public function testReturnProceedWhenCannotGetJoinPointObject() 54 | { 55 | $immutablePropertyCheck = new PropertyWriteTypeChecker(); 56 | $immutablePropertyCheck->__invoke($this->fieldAccess); 57 | } 58 | 59 | public function testReturnProceedWhenFieldAccessIsNotAWrite() 60 | { 61 | $this->fieldAccess->expects($this->any())->method('getAccessType')->will($this->returnValue(FieldAccess::READ)); 62 | 63 | $immutablePropertyCheck = new PropertyWriteTypeChecker(); 64 | $immutablePropertyCheck->__invoke($this->fieldAccess); 65 | } 66 | 67 | public function testWillFailOnInvalidAssignedType() 68 | { 69 | $field = new ReflectionProperty(ClassWithGenericArrayTypedProperty::class, 'property'); 70 | 71 | $this->fieldAccess->expects($this->any())->method('getAccessType')->will($this->returnValue(FieldAccess::WRITE)); 72 | $this->fieldAccess->expects($this->any())->method('getField')->will($this->returnValue($field)); 73 | $this->fieldAccess->expects($this->any())->method('getValueToSet')->will($this->returnValue('new value')); 74 | 75 | $immutablePropertyCheck = new PropertyWriteTypeChecker(); 76 | 77 | // catching the exception raised by PHPUnit by converting a fatal into an exception (in the error handler) 78 | $this->setExpectedException(\PHPUnit_Framework_Error::class); 79 | 80 | $immutablePropertyCheck->__invoke($this->fieldAccess); 81 | } 82 | } 83 | -------------------------------------------------------------------------------- /src/StrictPhp/TypeFinder/PropertyTypeFinder.php: -------------------------------------------------------------------------------- 1 | createFromReflector($reflectionProperty); 44 | 45 | return array_map( 46 | function (Type $type) use ($reflectionProperty, $contextClass) { 47 | return $this->expandSelfAndStaticTypes($type, $reflectionProperty, $contextClass); 48 | }, 49 | array_unique(array_filter(array_merge( 50 | [], 51 | ...array_map( 52 | function (VarTag $varTag) use ($typeResolver, $context) { 53 | return array_map( 54 | function ($type) use ($typeResolver, $context) { 55 | return $typeResolver->resolve($type, $context); 56 | }, 57 | $varTag->getTypes() 58 | ); 59 | }, 60 | (new DocBlock( 61 | $reflectionProperty, 62 | new DocBlock\Context($context->getNamespace(), $context->getNamespaceAliases()) 63 | )) 64 | ->getTagsByName('var') 65 | ) 66 | ))) 67 | ); 68 | } 69 | 70 | /** 71 | * Replaces "self", "$this" and "static" types with the corresponding runtime versions 72 | * 73 | * @todo may be removed if PHPDocumentor provides a runtime version of the types VO 74 | * 75 | * @param Type $type 76 | * @param ReflectionProperty $reflectionProperty 77 | * @param string $contextClass 78 | * 79 | * @return Type 80 | */ 81 | private function expandSelfAndStaticTypes(Type $type, ReflectionProperty $reflectionProperty, $contextClass) 82 | { 83 | if ($type instanceof Self_) { 84 | return new Object_(new Fqsen('\\' . $reflectionProperty->getDeclaringClass()->getName())); 85 | } 86 | 87 | if ($type instanceof Static_) { 88 | return new Object_(new Fqsen('\\' . $contextClass)); 89 | } 90 | 91 | return $type; 92 | } 93 | } 94 | -------------------------------------------------------------------------------- /tests/StrictPhpTest/Aspect/PropertyWriteAspectTest.php: -------------------------------------------------------------------------------- 1 | 28 | * @license MIT 29 | * 30 | * @group Coverage 31 | * 32 | * @covers \StrictPhp\Aspect\PropertyWriteAspect 33 | */ 34 | class PropertyWriteAspectTest extends \PHPUnit_Framework_TestCase 35 | { 36 | /** 37 | * @var FieldAccess|\PHPUnit_Framework_MockObject_MockObject 38 | */ 39 | private $fieldAccess; 40 | 41 | /** 42 | * @var \PHPUnit_Framework_MockObject_MockObject[]|callable[] 43 | */ 44 | private $callables = []; 45 | 46 | /** 47 | * {@inheritDoc} 48 | */ 49 | protected function setUp() 50 | { 51 | $this->fieldAccess = $this->getMock(FieldAccess::class); 52 | $this->callables = [ 53 | $this->getMock('stdClass', ['__invoke']), 54 | $this->getMock('stdClass', ['__invoke']), 55 | ]; 56 | } 57 | 58 | public function testWillSkipExecutionWhenNotWriteAccess() 59 | { 60 | $this->fieldAccess->expects($this->any())->method('proceed')->will($this->returnValue('done')); 61 | $this->fieldAccess->expects($this->once())->method('getAccessType')->will($this->returnValue(FieldAccess::READ)); 62 | 63 | foreach ($this->callables as $callable) { 64 | $callable->expects($this->never())->method('__invoke'); 65 | } 66 | 67 | $this->assertSame( 68 | 'done', 69 | (new PropertyWriteAspect(...$this->callables))->beforePropertyAccess($this->fieldAccess) 70 | ); 71 | } 72 | 73 | public function testWillExecuteOnWriteAccess() 74 | { 75 | $this->fieldAccess->expects($this->once())->method('proceed')->will($this->returnValue('done')); 76 | $this->fieldAccess->expects($this->once())->method('getAccessType')->will($this->returnValue(FieldAccess::WRITE)); 77 | 78 | 79 | foreach ($this->callables as $callable) { 80 | $callable->expects($this->once())->method('__invoke')->with($this->fieldAccess); 81 | } 82 | 83 | $this->assertSame( 84 | 'done', 85 | (new PropertyWriteAspect(...$this->callables))->beforePropertyAccess($this->fieldAccess) 86 | ); 87 | } 88 | 89 | public function testWillNotProceedExecuteOnWriteAndCrash() 90 | { 91 | $this->fieldAccess->expects($this->never())->method('proceed')->will($this->returnValue('done')); 92 | $this->fieldAccess->expects($this->once())->method('getAccessType')->will($this->returnValue(FieldAccess::WRITE)); 93 | 94 | foreach ($this->callables as $callable) { 95 | $callable->expects($this->any())->method('__invoke')->will($this->throwException(new \Exception())); 96 | } 97 | 98 | $aspect = new PropertyWriteAspect(...$this->callables); 99 | 100 | $this->setExpectedException(\Exception::class); 101 | 102 | $aspect->beforePropertyAccess($this->fieldAccess); 103 | } 104 | } 105 | -------------------------------------------------------------------------------- /tests/StrictPhpTest/Aspect/PostConstructAspectTest.php: -------------------------------------------------------------------------------- 1 | 29 | * @license MIT 30 | * 31 | * @group Coverage 32 | * 33 | * @covers \StrictPhp\Aspect\PostConstructAspect 34 | */ 35 | class PostConstructAspectTest extends \PHPUnit_Framework_TestCase 36 | { 37 | /** 38 | * @var MethodInvocation|\PHPUnit_Framework_MockObject_MockObject 39 | */ 40 | private $methodInvocation; 41 | 42 | /** 43 | * @var \PHPUnit_Framework_MockObject_MockObject[]|callable[] 44 | */ 45 | private $callables = []; 46 | 47 | /** 48 | * {@inheritDoc} 49 | */ 50 | protected function setUp() 51 | { 52 | $this->methodInvocation = $this->getMock(MethodInvocation::class); 53 | $this->callables = [ 54 | $this->getMock('stdClass', ['__invoke']), 55 | $this->getMock('stdClass', ['__invoke']), 56 | ]; 57 | } 58 | 59 | public function testWillExecuteOnWriteAccess() 60 | { 61 | $object = new \stdClass(); 62 | 63 | $this->methodInvocation->expects($this->once())->method('proceed')->will($this->returnValue('done')); 64 | $this->methodInvocation->expects($this->any())->method('getThis')->will($this->returnValue($object)); 65 | $this 66 | ->methodInvocation 67 | ->expects($this->any()) 68 | ->method('getMethod') 69 | ->will($this->returnValue(new ReflectionMethod(__CLASS__, __FUNCTION__))); 70 | 71 | 72 | foreach ($this->callables as $callable) { 73 | $callable->expects($this->once())->method('__invoke')->with($object, __CLASS__); 74 | } 75 | 76 | $this->assertSame( 77 | 'done', 78 | (new PostConstructAspect(...$this->callables))->postConstruct($this->methodInvocation) 79 | ); 80 | } 81 | 82 | public function testWillNotProceedExecuteOnWriteAndCrash() 83 | { 84 | $object = new \stdClass(); 85 | 86 | $this->methodInvocation->expects($this->never())->method('proceed')->will($this->returnValue('done')); 87 | $this->methodInvocation->expects($this->any())->method('getThis')->will($this->returnValue($object)); 88 | $this 89 | ->methodInvocation 90 | ->expects($this->any()) 91 | ->method('getMethod') 92 | ->will($this->returnValue(new ReflectionMethod(__CLASS__, __FUNCTION__))); 93 | 94 | foreach ($this->callables as $callable) { 95 | $callable 96 | ->expects($this->any()) 97 | ->method('__invoke') 98 | ->with($object, __CLASS__) 99 | ->will($this->throwException(new \Exception())); 100 | } 101 | 102 | $aspect = new PostConstructAspect(...$this->callables); 103 | 104 | $this->setExpectedException(\Exception::class); 105 | 106 | $aspect->postConstruct($this->methodInvocation); 107 | } 108 | } 109 | -------------------------------------------------------------------------------- /src/StrictPhp/AccessChecker/ParameterInterfaceJailer.php: -------------------------------------------------------------------------------- 1 | jailFactory = $jailFactory; 42 | } 43 | 44 | /** 45 | * Replaces the parameters within the given $methodInvocation with type-safe interface jails, whenever applicable 46 | * 47 | * @param AbstractMethodInvocation $methodInvocation 48 | * 49 | * @return void 50 | * 51 | * @throws ExceptionInterface 52 | * @throws HierarchyException 53 | */ 54 | public function __invoke(AbstractMethodInvocation $methodInvocation) 55 | { 56 | $method = $methodInvocation->getMethod(); 57 | $arguments = & Closure::bind( 58 | function & (AbstractMethodInvocation $methodInvocation) { 59 | return $methodInvocation->arguments; 60 | }, 61 | null, 62 | AbstractMethodInvocation::class 63 | )->__invoke($methodInvocation); 64 | 65 | foreach ($arguments as $parameterIndex => & $argument) { 66 | if (null === $argument) { 67 | continue; 68 | } 69 | 70 | if (! $interface = $this->getParameterInterfaceType($parameterIndex, $method)) { 71 | continue; 72 | } 73 | 74 | $argument = $this->jailFactory->createInstanceJail($argument, $interface); 75 | } 76 | } 77 | 78 | /** 79 | * @param int $index 80 | * @param ReflectionMethod|null $reflectionMethod 81 | * 82 | * @return ReflectionParameter 83 | */ 84 | private function getParameterInterfaceType($index, ReflectionMethod $reflectionMethod = null) 85 | { 86 | $parameters = $reflectionMethod ? $reflectionMethod->getParameters() : []; 87 | 88 | if (isset($parameters[$index])) { 89 | return $this->getInterface($parameters[$index]); 90 | } 91 | 92 | /* @var $lastParameter \ReflectionParameter|null */ 93 | $lastParameter = end($parameters); 94 | 95 | return $lastParameter && $lastParameter->isVariadic() 96 | ? $this->getInterface($lastParameter) 97 | : null; 98 | } 99 | 100 | /** 101 | * @param ReflectionParameter $parameter 102 | * 103 | * @return string|null 104 | */ 105 | private function getInterface(ReflectionParameter $parameter) 106 | { 107 | return ($class = $parameter->getClass()) 108 | && $class->isInterface() 109 | ? $class->getName() 110 | : null; 111 | } 112 | } 113 | -------------------------------------------------------------------------------- /tests/StrictPhpTest/AccessChecker/ReturnTypeCheckerTest.php: -------------------------------------------------------------------------------- 1 | 36 | * @license MIT 37 | * 38 | * @group Coverage 39 | * 40 | * @covers \StrictPhp\AccessChecker\ReturnTypeChecker 41 | */ 42 | class ReturnTypeCheckerTest extends \PHPUnit_Framework_TestCase 43 | { 44 | /** 45 | * @var ParameterTypeChecker 46 | */ 47 | private $parameterCheck; 48 | 49 | /** 50 | * @var callable|\PHPUnit_Framework_MockObject_MockObject 51 | */ 52 | private $applyTypeChecks; 53 | 54 | /** 55 | * {@inheritDoc} 56 | */ 57 | protected function setUp() 58 | { 59 | $this->applyTypeChecks = $this->getMock(stdClass::class, ['__invoke']); 60 | $this->parameterCheck = new ReturnTypeChecker($this->applyTypeChecks); 61 | } 62 | 63 | public function testReturnSimpleTypeChecker() 64 | { 65 | /* @var MethodInvocation|\PHPUnit_Framework_MockObject_MockObject $method */ 66 | $method = $this->getMock(MethodInvocation::class); 67 | 68 | $reflectionMethod = new ReflectionMethod(ClassWithReturnTypeMethod::class, 'expectString'); 69 | 70 | $method->expects($this->exactly(2))->method('getMethod')->willReturn($reflectionMethod); 71 | 72 | $parameterCheck = $this->parameterCheck; 73 | 74 | $this 75 | ->applyTypeChecks 76 | ->expects($this->once()) 77 | ->method('__invoke') 78 | ->with( 79 | $this->callback(function (array $types) { 80 | return (bool) array_map( 81 | function (Type $type) { 82 | $this->assertInstanceOf(String_::class, $type); 83 | }, 84 | $types 85 | ); 86 | }) 87 | ); 88 | 89 | $parameterCheck($method); 90 | } 91 | 92 | public function testReturnCompostTypeChecker() 93 | { 94 | /* @var MethodInvocation|\PHPUnit_Framework_MockObject_MockObject $method */ 95 | $method = $this->getMock(MethodInvocation::class); 96 | 97 | $reflectionMethod = new ReflectionMethod(ClassWithReturnTypeMethod::class, 'expectMixedDataCollection'); 98 | 99 | $method->expects($this->exactly(2))->method('getMethod')->willReturn($reflectionMethod); 100 | 101 | $expected = [ 102 | new Array_(new Array_(new String_())), 103 | new Array_(new Boolean()), 104 | ]; 105 | 106 | $this 107 | ->applyTypeChecks 108 | ->expects($this->once()) 109 | ->method('__invoke') 110 | ->with( 111 | $this->equalTo($expected) 112 | ); 113 | 114 | $parameterCheck = $this->parameterCheck; 115 | 116 | $parameterCheck($method); 117 | } 118 | } 119 | -------------------------------------------------------------------------------- /src/StrictPhp/TypeFinder/ReturnTypeFinder.php: -------------------------------------------------------------------------------- 1 | 36 | */ 37 | final class ReturnTypeFinder 38 | { 39 | /** 40 | * @param ReflectionMethod $reflectionMethod 41 | * @param string $contextClass 42 | * 43 | * @return Type[] 44 | */ 45 | public function __invoke(ReflectionMethod $reflectionMethod, $contextClass) 46 | { 47 | $typeResolver = new TypeResolver(); 48 | $context = (new ContextFactory())->createFromReflector($reflectionMethod); 49 | 50 | return array_map( 51 | function (Type $type) use ($reflectionMethod, $contextClass) { 52 | return $this->expandSelfAndStaticTypes($type, $reflectionMethod, $contextClass); 53 | }, 54 | array_unique(array_filter(array_merge( 55 | [], 56 | ...array_map( 57 | function (ReturnTag $varTag) use ($typeResolver, $context) { 58 | return array_map( 59 | function ($type) use ($typeResolver, $context) { 60 | return $typeResolver->resolve($type, $context); 61 | }, 62 | $varTag->getTypes() 63 | ); 64 | }, 65 | $this->getReturnTagForMethod($reflectionMethod, $context) 66 | ) 67 | ))) 68 | ); 69 | } 70 | 71 | /** 72 | * Replaces "self", "$this" and "static" types with the corresponding runtime versions 73 | * 74 | * @todo may be removed if PHPDocumentor provides a runtime version of the types VO 75 | * 76 | * @param Type $type 77 | * @param ReflectionMethod $reflectionMethod 78 | * @param string $contextClass 79 | * 80 | * @return Type 81 | */ 82 | private function expandSelfAndStaticTypes(Type $type, ReflectionMethod $reflectionMethod, $contextClass) 83 | { 84 | if ($type instanceof Self_ || $type instanceof This) { 85 | return new Object_(new Fqsen('\\' . $reflectionMethod->getDeclaringClass()->getName())); 86 | } 87 | 88 | if ($type instanceof Static_) { 89 | return new Object_(new Fqsen('\\' . $contextClass)); 90 | } 91 | 92 | return $type; 93 | } 94 | 95 | /** 96 | * @param ReflectionMethod $reflectionMethod 97 | * @param Context $context 98 | * 99 | * @return ReturnTag[] 100 | */ 101 | private function getReturnTagForMethod(ReflectionMethod $reflectionMethod, Context $context) 102 | { 103 | return (new DocBlock( 104 | $reflectionMethod, 105 | new DocBlock\Context($context->getNamespace(), $context->getNamespaceAliases()) 106 | )) 107 | ->getTagsByName('return'); 108 | } 109 | } 110 | -------------------------------------------------------------------------------- /tests/StrictPhpTest/TypeChecker/TypeChecker/NullTypeCheckerTest.php: -------------------------------------------------------------------------------- 1 | 35 | * @license MIT 36 | * 37 | * @group Coverage 38 | * 39 | * @covers \StrictPhp\TypeChecker\TypeChecker\NullTypeChecker 40 | */ 41 | class NullTypeCheckerTest extends \PHPUnit_Framework_TestCase 42 | { 43 | /** 44 | * @var NullTypeChecker 45 | */ 46 | private $nullCheck; 47 | 48 | /** 49 | * {@inheritDoc} 50 | */ 51 | public function setUp() 52 | { 53 | $this->nullCheck = new NullTypeChecker(); 54 | } 55 | 56 | public function testRejectsNotNullType() 57 | { 58 | $this->setExpectedException(\InvalidArgumentException::class); 59 | 60 | $this->nullCheck->validate(null, new Integer()); 61 | } 62 | 63 | /** 64 | * @dataProvider mixedDataTypes 65 | * 66 | * @param Type $type 67 | * @param boolean $expected 68 | */ 69 | public function testTypeCanBeApplied(Type $type, $expected) 70 | { 71 | $this->assertSame($expected, $this->nullCheck->canApplyToType($type)); 72 | } 73 | 74 | /** 75 | * @covers \StrictPhp\TypeChecker\TypeChecker\IntegerTypeChecker::validate 76 | * 77 | * @dataProvider mixedDataTypesToValidate 78 | * 79 | * @param string $value 80 | * @param boolean $expected 81 | */ 82 | public function testIfDataTypeIsValid($value, $expected) 83 | { 84 | $this->assertSame($expected, $this->nullCheck->validate($value, new Null_())); 85 | } 86 | 87 | public function testSimulateFailureRaisesExceptionWhenNotPassAString() 88 | { 89 | $this->setExpectedException(\ErrorException::class); 90 | 91 | $this->nullCheck->simulateFailure([], new Null_()); 92 | } 93 | 94 | public function testSimulateFailureDoesNothingWhenPassAString() 95 | { 96 | $this->nullCheck->simulateFailure(null, new Null_()); 97 | } 98 | 99 | /** 100 | * @return mixed[][] - mixed type 101 | * - boolean expected 102 | * 103 | */ 104 | public function mixedDataTypesToValidate() 105 | { 106 | return [ 107 | [123, false], 108 | [0x12, false], 109 | [new \StdClass, false], 110 | ['Marco Pivetta', false], 111 | [[], false], 112 | [true, false], 113 | [null, true], 114 | ]; 115 | } 116 | 117 | /** 118 | * @return mixed[][] - string with type of data 119 | * - expected output 120 | */ 121 | public function mixedDataTypes() 122 | { 123 | return [ 124 | [new Null_(), true], 125 | [new Integer(), false], 126 | [new Object_(), false], 127 | [new String_(), false], 128 | [new Array_(), false], 129 | [new Boolean(), false], 130 | [new Mixed(), false], 131 | ]; 132 | } 133 | } 134 | -------------------------------------------------------------------------------- /tests/StrictPhpTest/TypeChecker/TypeChecker/MixedTypeCheckerTest.php: -------------------------------------------------------------------------------- 1 | 36 | * @license MIT 37 | * 38 | * @group Coverage 39 | * 40 | * @covers \StrictPhp\TypeChecker\TypeChecker\MixedTypeChecker 41 | */ 42 | class MixedTypeCheckerTest extends \PHPUnit_Framework_TestCase 43 | { 44 | /** 45 | * @var MixedTypeChecker 46 | */ 47 | private $mixedChecker; 48 | 49 | /** 50 | * {@inheritDoc} 51 | */ 52 | public function setUp() 53 | { 54 | $this->mixedChecker = new MixedTypeChecker(); 55 | } 56 | 57 | /** 58 | * @dataProvider mixedDataTypes 59 | * 60 | * @param Type $type 61 | * @param boolean $expected 62 | */ 63 | public function testTypeCanBeApplied(Type $type, $expected) 64 | { 65 | $this->assertSame($expected, $this->mixedChecker->canApplyToType($type)); 66 | } 67 | 68 | /** 69 | * @dataProvider mixedDataTypesToValidate 70 | * 71 | * @param string $value 72 | * @param boolean $expected 73 | */ 74 | public function testIfDataTypeIsValid($value, $expected) 75 | { 76 | $this->assertSame($expected, $this->mixedChecker->validate($value, new Mixed())); 77 | } 78 | 79 | public function testRejectsInvalidTypeValidation() 80 | { 81 | $this->setExpectedException(\InvalidArgumentException::class); 82 | 83 | $this->mixedChecker->validate('foo', new String_()); 84 | } 85 | 86 | public function testSimulateSuccess() 87 | { 88 | $this->mixedChecker->simulateFailure([], new Mixed()); 89 | 90 | // @TODO assertion? 91 | } 92 | 93 | /** 94 | * @return mixed[][] - mixed type 95 | * - boolean expected 96 | * 97 | */ 98 | public function mixedDataTypesToValidate() 99 | { 100 | return [ 101 | [[], true], 102 | [new \StdClass, true], 103 | [true, true], 104 | [null, true], 105 | [123, true], 106 | [1e-3, true], 107 | [0x12, true], 108 | ['Marco Pivetta', true], 109 | ]; 110 | } 111 | 112 | /** 113 | * @return mixed[][] - string with type of data 114 | * - expected output 115 | */ 116 | public function mixedDataTypes() 117 | { 118 | return [ 119 | [new Mixed(), true], 120 | [new Array_(), false], 121 | [new String_(), false], 122 | [new Object_(), false], 123 | [new Object_(new Fqsen('\\' . __CLASS__)), false], 124 | [new Boolean(), false], 125 | [new Integer(), false], 126 | [new Null_(), false], 127 | ]; 128 | } 129 | } 130 | -------------------------------------------------------------------------------- /tests/StrictPhpTest/TypeChecker/TypeChecker/IntegerTypeCheckerTest.php: -------------------------------------------------------------------------------- 1 | 35 | * @license MIT 36 | * 37 | * @group Coverage 38 | */ 39 | class IntegerTypeCheckerTest extends \PHPUnit_Framework_TestCase 40 | { 41 | /** 42 | * @var IntegerTypeChecker 43 | */ 44 | private $integerCheck; 45 | 46 | /** 47 | * {@inheritDoc} 48 | */ 49 | public function setUp() 50 | { 51 | $this->integerCheck = new IntegerTypeChecker(); 52 | } 53 | 54 | /** 55 | * @covers \StrictPhp\TypeChecker\TypeChecker\IntegerTypeChecker::canApplyToType 56 | * 57 | * @dataProvider mixedDataTypes 58 | * 59 | * @param Type $type 60 | * @param boolean $expected 61 | */ 62 | public function testTypeCanBeApplied(Type $type, $expected) 63 | { 64 | $this->assertSame($expected, $this->integerCheck->canApplyToType($type)); 65 | } 66 | 67 | /** 68 | * @covers \StrictPhp\TypeChecker\TypeChecker\IntegerTypeChecker::validate 69 | * 70 | * @dataProvider mixedDataTypesToValidate 71 | * 72 | * @param string $value 73 | * @param boolean $expected 74 | */ 75 | public function testIfDataTypeIsValid($value, $expected) 76 | { 77 | $this->assertSame($expected, $this->integerCheck->validate($value, new Integer())); 78 | } 79 | 80 | /** 81 | * @covers \StrictPhp\TypeChecker\TypeChecker\IntegerTypeChecker::simulateFailure 82 | */ 83 | public function testSimulateFailureRaisesExceptionWhenNotPassAString() 84 | { 85 | $this->setExpectedException(\ErrorException::class); 86 | $this->integerCheck->simulateFailure([], new Integer()); 87 | } 88 | 89 | /** 90 | * @covers \StrictPhp\TypeChecker\TypeChecker\IntegerTypeChecker::simulateFailure 91 | */ 92 | public function testSimulateFailureDoesNothingWhenPassAString() 93 | { 94 | $this->integerCheck->simulateFailure(10, new Integer()); 95 | } 96 | 97 | /** 98 | * @return mixed[][] - mixed type 99 | * - boolean expected 100 | * 101 | */ 102 | public function mixedDataTypesToValidate() 103 | { 104 | return [ 105 | [123, true], 106 | [0x12, true], 107 | [new \StdClass, false], 108 | ['Marco Pivetta', false], 109 | [[], false], 110 | [true, false], 111 | [null, false], 112 | ]; 113 | } 114 | 115 | /** 116 | * @return mixed[][] - string with type of data 117 | * - expected output 118 | */ 119 | public function mixedDataTypes() 120 | { 121 | return [ 122 | [new Integer(), true], 123 | [new Object_(), false], 124 | [new String_(), false], 125 | [new Array_(), false], 126 | [new Boolean(), false], 127 | [new Null_(), false], 128 | [new Mixed(), false], 129 | ]; 130 | } 131 | } 132 | -------------------------------------------------------------------------------- /tests/StrictPhpTest/TypeChecker/TypeChecker/BooleanTypeCheckerTest.php: -------------------------------------------------------------------------------- 1 | 35 | * @license MIT 36 | * 37 | * @group Coverage 38 | * 39 | * @covers \StrictPhp\TypeChecker\TypeChecker\BooleanTypeChecker 40 | */ 41 | class BooleanTypeCheckerTest extends \PHPUnit_Framework_TestCase 42 | { 43 | /** 44 | * @var BooleanTypeChecker 45 | */ 46 | private $booleanCheck; 47 | 48 | /** 49 | * {@inheritDoc} 50 | */ 51 | public function setUp() 52 | { 53 | $this->booleanCheck = new BooleanTypeChecker(); 54 | } 55 | 56 | /** 57 | * @dataProvider mixedDataTypes 58 | * 59 | * @param Type $type 60 | * @param boolean $expected 61 | */ 62 | public function testTypeCanBeApplied(Type $type, $expected) 63 | { 64 | $this->assertSame($expected, $this->booleanCheck->canApplyToType($type)); 65 | } 66 | 67 | /** 68 | * @dataProvider mixedDataTypesToValidate 69 | * 70 | * @param string $value 71 | * @param boolean $expected 72 | */ 73 | public function testIfDataTypeIsValid($value, $expected) 74 | { 75 | $this->assertSame($expected, $this->booleanCheck->validate($value, new Boolean())); 76 | } 77 | 78 | public function testSimulateFailureRaisesExceptionWhenNotPassingABoolean() 79 | { 80 | $this->setExpectedException(\ErrorException::class); 81 | 82 | $this->booleanCheck->simulateFailure(0, new Boolean()); 83 | } 84 | 85 | public function testSimulateFailureDoesNothingWhenPassingABoolean() 86 | { 87 | $this->booleanCheck->simulateFailure(true, new Boolean()); 88 | $this->booleanCheck->simulateFailure(false, new Boolean()); 89 | } 90 | 91 | public function testRejectsValidatingWithNonBooleanType() 92 | { 93 | $this->setExpectedException(\InvalidArgumentException::class); 94 | 95 | $this->booleanCheck->validate(true, new Array_()); 96 | } 97 | 98 | /** 99 | * @return mixed[][] - mixed type 100 | * - boolean expected 101 | * 102 | */ 103 | public function mixedDataTypesToValidate() 104 | { 105 | return [ 106 | [true, true], 107 | [false, true], 108 | [123, false], 109 | [0x12, false], 110 | [new \StdClass, false], 111 | ['Marco Pivetta', false], 112 | [[], false], 113 | [null, false], 114 | ]; 115 | } 116 | 117 | /** 118 | * @return mixed[][] - string with type of data 119 | * - expected output 120 | */ 121 | public function mixedDataTypes() 122 | { 123 | return [ 124 | [new Boolean(), true], 125 | [new Integer(), false], 126 | [new Object_(), false], 127 | [new String_(), false], 128 | [new Array_(), false], 129 | [new Null_(), false], 130 | [new Mixed(), false], 131 | ]; 132 | } 133 | } 134 | -------------------------------------------------------------------------------- /tests/StrictPhpTest/TypeChecker/TypeChecker/ResourceTypeCheckerTest.php: -------------------------------------------------------------------------------- 1 | 36 | * @license MIT 37 | * 38 | * @group Coverage 39 | * 40 | * @covers \StrictPhp\TypeChecker\TypeChecker\ResourceTypeChecker 41 | */ 42 | class ResourceTypeCheckerTest extends \PHPUnit_Framework_TestCase 43 | { 44 | /** 45 | * @var ResourceTypeChecker 46 | */ 47 | private $resourceCheck; 48 | 49 | /** 50 | * {@inheritDoc} 51 | */ 52 | public function setUp() 53 | { 54 | $this->resourceCheck = new ResourceTypeChecker(); 55 | } 56 | 57 | /** 58 | * @dataProvider mixedDataTypes 59 | * 60 | * @param Type $type 61 | * @param boolean $expected 62 | */ 63 | public function testTypeCanBeApplied(Type $type, $expected) 64 | { 65 | $this->assertSame($expected, $this->resourceCheck->canApplyToType($type)); 66 | } 67 | 68 | /** 69 | * @dataProvider mixedDataTypesToValidate 70 | * 71 | * @param string $value 72 | * @param boolean $expected 73 | */ 74 | public function testIfDataTypeIsValid($value, $expected) 75 | { 76 | $this->assertSame($expected, $this->resourceCheck->validate($value, new Resource())); 77 | } 78 | 79 | public function testSimulateFailureRaisesExceptionWhenNotPassingAResource() 80 | { 81 | $this->setExpectedException(\ErrorException::class); 82 | 83 | $this->resourceCheck->simulateFailure(0, new Resource()); 84 | } 85 | 86 | public function testSimulateFailureDoesNothingWhenPassingAResource() 87 | { 88 | $this->resourceCheck->simulateFailure(tmpfile(), new Resource()); 89 | } 90 | 91 | public function testRejectsValidatingWithNonResourceType() 92 | { 93 | $this->setExpectedException(\InvalidArgumentException::class); 94 | 95 | $this->resourceCheck->validate(tmpfile(), new Array_()); 96 | } 97 | 98 | /** 99 | * @return mixed[][] - mixed type 100 | * - boolean expected 101 | * 102 | */ 103 | public function mixedDataTypesToValidate() 104 | { 105 | return [ 106 | [tmpfile(), true], 107 | [true, false], 108 | [false, false], 109 | [123, false], 110 | [0x12, false], 111 | [new \StdClass, false], 112 | ['Marco Pivetta', false], 113 | [[], false], 114 | [null, false], 115 | ]; 116 | } 117 | 118 | /** 119 | * @return mixed[][] - string with type of data 120 | * - expected output 121 | */ 122 | public function mixedDataTypes() 123 | { 124 | return [ 125 | [new Resource(), true], 126 | [new Boolean(), false], 127 | [new Integer(), false], 128 | [new Object_(), false], 129 | [new String_(), false], 130 | [new Array_(), false], 131 | [new Null_(), false], 132 | [new Mixed(), false], 133 | ]; 134 | } 135 | } 136 | -------------------------------------------------------------------------------- /src/StrictPhp/TypeFinder/ParameterTypeFinder.php: -------------------------------------------------------------------------------- 1 | createFromReflector($reflectionParameter); 45 | 46 | return array_map( 47 | function (Type $type) use ($reflectionParameter, $contextClass) { 48 | return $this->expandSelfAndStaticTypes($type, $reflectionParameter, $contextClass); 49 | }, 50 | array_unique(array_filter(array_merge( 51 | [], 52 | ...array_map( 53 | function (ParamTag $varTag) use ($typeResolver, $context) { 54 | return array_map( 55 | function ($type) use ($typeResolver, $context) { 56 | return $typeResolver->resolve($type, $context); 57 | }, 58 | $varTag->getTypes() 59 | ); 60 | }, 61 | $this->getParamTagsForParameter($reflectionParameter, $context) 62 | ) 63 | ))) 64 | ); 65 | } 66 | 67 | /** 68 | * Replaces "self", "$this" and "static" types with the corresponding runtime versions 69 | * 70 | * @todo may be removed if PHPDocumentor provides a runtime version of the types VO 71 | * 72 | * @param Type $type 73 | * @param ReflectionParameter $reflectionParameter 74 | * @param string $contextClass 75 | * 76 | * @return Type 77 | */ 78 | private function expandSelfAndStaticTypes(Type $type, ReflectionParameter $reflectionParameter, $contextClass) 79 | { 80 | if ($type instanceof Self_) { 81 | return new Object_(new Fqsen('\\' . $reflectionParameter->getDeclaringClass()->getName())); 82 | } 83 | 84 | if ($type instanceof Static_) { 85 | return new Object_(new Fqsen('\\' . $contextClass)); 86 | } 87 | 88 | return $type; 89 | } 90 | 91 | /** 92 | * @param ReflectionParameter $reflectionParameter 93 | * @param Context $context 94 | * 95 | * @return ParamTag[] 96 | */ 97 | private function getParamTagsForParameter(ReflectionParameter $reflectionParameter, Context $context) 98 | { 99 | $reflectionFunction = $reflectionParameter->getDeclaringFunction(); 100 | $parameterName = $reflectionParameter->getName(); 101 | 102 | return array_filter( 103 | (new DocBlock( 104 | $reflectionFunction, 105 | new DocBlock\Context($context->getNamespace(), $context->getNamespaceAliases()) 106 | )) 107 | ->getTagsByName('param'), 108 | function (ParamTag $paramTag) use ($parameterName) { 109 | return ltrim($paramTag->getVariableName(), '$') === $parameterName; 110 | } 111 | ); 112 | } 113 | } 114 | -------------------------------------------------------------------------------- /tests/StrictPhpTest/TypeChecker/TypeChecker/CallableTypeCheckerTest.php: -------------------------------------------------------------------------------- 1 | 36 | * @license MIT 37 | * 38 | * @group Coverage 39 | */ 40 | class CallableTypeCheckerTest extends \PHPUnit_Framework_TestCase 41 | { 42 | /** 43 | * @var CallableTypeChecker 44 | */ 45 | private $callableCheck; 46 | 47 | /** 48 | * {@inheritDoc} 49 | */ 50 | public function setUp() 51 | { 52 | $this->callableCheck = new CallableTypeChecker(); 53 | } 54 | 55 | /** 56 | * @covers \StrictPhp\TypeChecker\TypeChecker\CallableTypeChecker::canApplyToType 57 | * 58 | * @dataProvider mixedDataTypes 59 | * 60 | * @param Type $type 61 | * @param boolean $expected 62 | */ 63 | public function testTypeCanBeApplied(Type $type, $expected) 64 | { 65 | $this->assertSame($expected, $this->callableCheck->canApplyToType($type)); 66 | } 67 | 68 | /** 69 | * @covers \StrictPhp\TypeChecker\TypeChecker\CallableTypeChecker::validate 70 | * 71 | * @dataProvider mixedDataTypesToValidate 72 | * 73 | * @param string $value 74 | * @param boolean $expected 75 | */ 76 | public function testIfDataTypeIsValid($value, $expected) 77 | { 78 | $this->assertSame($expected, $this->callableCheck->validate($value, new Callable_())); 79 | } 80 | 81 | /** 82 | * @covers \StrictPhp\TypeChecker\TypeChecker\CallableTypeChecker::simulateFailure 83 | */ 84 | public function testSimulateFailure() 85 | { 86 | $this->assertAttributeEmpty('failingCallback', $this->callableCheck); 87 | $this->callableCheck->simulateFailure( 88 | function () { 89 | }, 90 | new Callable_() 91 | ); 92 | $this->assertAttributeNotEmpty('failingCallback', $this->callableCheck); 93 | $this->assertAttributeInternalType('callable', 'failingCallback', $this->callableCheck); 94 | } 95 | 96 | /** 97 | * @return mixed[][] - mixed type 98 | * - boolean expected 99 | * 100 | */ 101 | public function mixedDataTypesToValidate() 102 | { 103 | return [ 104 | [ 105 | function () { 106 | }, 107 | true 108 | ], 109 | [[], false], 110 | [new \StdClass, false], 111 | [true, false], 112 | [null, false], 113 | [123, false], 114 | [1e-3, false], 115 | [0x12, false], 116 | ['Marco Pivetta', false], 117 | ]; 118 | } 119 | 120 | /** 121 | * @return mixed[][] - string with type of data 122 | * - expected output 123 | */ 124 | public function mixedDataTypes() 125 | { 126 | return [ 127 | [new Callable_(), true], 128 | [new Array_(), false], 129 | [new String_(), false], 130 | [new Object_(), false], 131 | [new Boolean(), false], 132 | [new Integer(), false], 133 | [new Null_(), false], 134 | [new Mixed(), false], 135 | ]; 136 | } 137 | } 138 | -------------------------------------------------------------------------------- /tests/StrictPhpTest/TypeChecker/TypeChecker/StringTypeCheckerTest.php: -------------------------------------------------------------------------------- 1 | 36 | * @license MIT 37 | * 38 | * @group Coverage 39 | */ 40 | class StringTypeCheckerTest extends \PHPUnit_Framework_TestCase 41 | { 42 | /** 43 | * @var StringTypeChecker 44 | */ 45 | private $stringCheck; 46 | 47 | /** 48 | * {@inheritDoc} 49 | */ 50 | public function setUp() 51 | { 52 | $this->stringCheck = new StringTypeChecker(); 53 | } 54 | 55 | /** 56 | * @covers \StrictPhp\TypeChecker\TypeChecker\StringTypeChecker::canApplyToType 57 | * 58 | * @dataProvider mixedDataTypes 59 | * 60 | * @param Type $type 61 | * @param boolean $expected 62 | */ 63 | public function testTypeCanBeApplied(Type $type, $expected) 64 | { 65 | $this->assertSame($expected, $this->stringCheck->canApplyToType($type)); 66 | } 67 | 68 | /** 69 | * @covers \StrictPhp\TypeChecker\TypeChecker\StringTypeChecker::validate 70 | * 71 | * @dataProvider mixedDataTypesToValidate 72 | * 73 | * @param string $value 74 | * @param boolean $expected 75 | */ 76 | public function testIfDataTypeIsValid($value, $expected) 77 | { 78 | $this->assertSame($expected, $this->stringCheck->validate($value, new String_())); 79 | } 80 | 81 | /** 82 | * @covers \StrictPhp\TypeChecker\TypeChecker\StringTypeChecker::simulateFailure 83 | */ 84 | public function testSimulateFailureRaisesExceptionWhenNotPassAString() 85 | { 86 | $this->setExpectedException(\ErrorException::class); 87 | $this->stringCheck->simulateFailure([], new String_()); 88 | } 89 | 90 | /** 91 | * @covers \StrictPhp\TypeChecker\TypeChecker\StringTypeChecker::simulateFailure 92 | */ 93 | public function testSimulateFailureDoesNothingWhenPassAString() 94 | { 95 | $this->stringCheck->simulateFailure('Marco Pivetta', new String_()); 96 | 97 | // @TODO add assertion 98 | } 99 | 100 | /** 101 | * @return mixed[][] - mixed type 102 | * - boolean expected 103 | * 104 | */ 105 | public function mixedDataTypesToValidate() 106 | { 107 | return [ 108 | ['Marco Pivetta', true], 109 | [[], false], 110 | [new \StdClass, false], 111 | [true, false], 112 | [null, false], 113 | [123, false], 114 | [1e-3, false], 115 | [0x12, false], 116 | ]; 117 | } 118 | 119 | /** 120 | * @return mixed[][] - string with type of data 121 | * - expected output 122 | */ 123 | public function mixedDataTypes() 124 | { 125 | return [ 126 | [new String_(), true], 127 | [new Array_(), false], 128 | [new Object_(), false], 129 | [new Object_(new Fqsen('\\' . __CLASS__)), false], 130 | [new Boolean(), false], 131 | [new Integer(), false], 132 | [new Null_(), false], 133 | [new Mixed(), false], 134 | ]; 135 | } 136 | } 137 | 138 | -------------------------------------------------------------------------------- /tests/StrictPhpTest/TypeChecker/TypeChecker/GenericObjectTypeCheckerTest.php: -------------------------------------------------------------------------------- 1 | 36 | * @license MIT 37 | * 38 | * @group Coverage 39 | */ 40 | class GenericObjectTypeCheckerTest extends \PHPUnit_Framework_TestCase 41 | { 42 | /** 43 | * @var GenericObjectTypeChecker 44 | */ 45 | private $objectCheck; 46 | 47 | /** 48 | * {@inheritDoc} 49 | */ 50 | public function setUp() 51 | { 52 | $this->objectCheck = new GenericObjectTypeChecker(); 53 | } 54 | 55 | /** 56 | * @covers \StrictPhp\TypeChecker\TypeChecker\GenericObjectTypeChecker::canApplyToType 57 | * 58 | * @dataProvider mixedDataTypes 59 | * 60 | * @param Type $type 61 | * @param boolean $expected 62 | */ 63 | public function testTypeCanBeApplied(Type $type, $expected) 64 | { 65 | $this->assertSame($expected, $this->objectCheck->canApplyToType($type)); 66 | } 67 | 68 | /** 69 | * @covers \StrictPhp\TypeChecker\TypeChecker\GenericObjectTypeChecker::validate 70 | * 71 | * @dataProvider mixedDataTypesToValidate 72 | * 73 | * @param string $value 74 | * @param boolean $expected 75 | */ 76 | public function testIfDataTypeIsValid($value, $expected) 77 | { 78 | $this->assertSame($expected, $this->objectCheck->validate($value, new Object_())); 79 | } 80 | 81 | /** 82 | * @covers \StrictPhp\TypeChecker\TypeChecker\GenericObjectTypeChecker::simulateFailure 83 | */ 84 | public function testSimulateFailureRaisesExceptionWhenNotPassAString() 85 | { 86 | $this->setExpectedException(\ErrorException::class); 87 | $this->objectCheck->simulateFailure([], new Object_()); 88 | } 89 | 90 | /** 91 | * @covers \StrictPhp\TypeChecker\TypeChecker\GenericObjectTypeChecker::simulateFailure 92 | */ 93 | public function testSimulateFailureDoesNothingWhenPassAString() 94 | { 95 | $this->objectCheck->simulateFailure(new \StdClass, new Object_()); 96 | 97 | // @TODO add assertion here 98 | } 99 | 100 | /** 101 | * @return mixed[][] - mixed type 102 | * - boolean expected 103 | * 104 | */ 105 | public function mixedDataTypesToValidate() 106 | { 107 | return [ 108 | [new \StdClass, true], 109 | ['Marco Pivetta', false], 110 | [[], false], 111 | [true, false], 112 | [null, false], 113 | [123, false], 114 | [1e-3, false], 115 | [0x12, false], 116 | ]; 117 | } 118 | 119 | /** 120 | * @return mixed[][] - string with type of data 121 | * - expected output 122 | */ 123 | public function mixedDataTypes() 124 | { 125 | return [ 126 | [new Object_(), true], 127 | [new Object_(new Fqsen('\\' . __CLASS__)), false], 128 | [new String_(), false], 129 | [new Array_(), false], 130 | [new Boolean(), false], 131 | [new Integer(), false], 132 | [new Null_(), false], 133 | [new Mixed(), false], 134 | ]; 135 | } 136 | } 137 | -------------------------------------------------------------------------------- /src/StrictPhp/TypeChecker/TypeChecker/TypedTraversableChecker.php: -------------------------------------------------------------------------------- 1 | typeCheckers = array_merge($typeCheckers, [$this]); 39 | } 40 | 41 | /** 42 | * {@inheritDoc} 43 | */ 44 | public function canApplyToType(Type $type) 45 | { 46 | // @todo validate also key type! 47 | return $type instanceof Array_ 48 | && $this->getCheckersApplicableToType($type->getValueType()); 49 | } 50 | 51 | /** 52 | * {@inheritDoc} 53 | * 54 | * @throws \InvalidArgumentException 55 | */ 56 | public function validate($value, Type $type) 57 | { 58 | if (! $type instanceof Array_) { 59 | throw new \InvalidArgumentException(sprintf('Invalid type "%s" provided', get_class($type))); 60 | } 61 | 62 | return (($value instanceof \Traversable) || is_array($value)) 63 | && $this->getCheckersValidForType($value, $type->getValueType()); 64 | } 65 | 66 | /** 67 | * {@inheritDoc} 68 | * 69 | * @throws \InvalidArgumentException 70 | * @throws \ErrorException 71 | */ 72 | public function simulateFailure($value, Type $type) 73 | { 74 | if (! $type instanceof Array_) { 75 | throw new \InvalidArgumentException(sprintf( 76 | 'Expecting type of type "%s", "%s" given', 77 | Array_::class, 78 | get_class($type) 79 | )); 80 | } 81 | 82 | if ($value instanceof \Traversable) { 83 | // cannot traverse an iterator, as it may have multiple dangerous side-effects 84 | 85 | // @link https://github.com/Roave/StrictPhp/issues/16 86 | 87 | return; 88 | } 89 | 90 | /* @var $value array */ 91 | $this->simulateFailuresOverArray($value, $type->getValueType()); 92 | } 93 | 94 | /** 95 | * @param array $value 96 | * @param Type $subType 97 | * 98 | * @return void 99 | * 100 | * @throws \ErrorException 101 | */ 102 | private function simulateFailuresOverArray(array $value, Type $subType) 103 | { 104 | foreach ($this->getCheckersApplicableToType($subType) as $typeChecker) { 105 | foreach ($value as $singleValue) { 106 | $typeChecker->simulateFailure($singleValue, $subType); 107 | } 108 | } 109 | } 110 | 111 | /** 112 | * @param Type $type 113 | * 114 | * @return TypeCheckerInterface[] 115 | */ 116 | private function getCheckersApplicableToType(Type $type) 117 | { 118 | return array_filter( 119 | $this->typeCheckers, 120 | function (TypeCheckerInterface $typeChecker) use ($type) { 121 | return $typeChecker->canApplyToType($type); 122 | } 123 | ); 124 | } 125 | 126 | /** 127 | * @param array|\Traversable $values 128 | * @param Type $type 129 | * 130 | * @return TypeCheckerInterface[] 131 | */ 132 | private function getCheckersValidForType($values, Type $type) 133 | { 134 | if ($values instanceof \Traversable) { 135 | return [new MixedTypeChecker()]; 136 | } 137 | 138 | return array_filter( 139 | $this->getCheckersApplicableToType($type), 140 | function (TypeCheckerInterface $typeChecker) use ($values, $type) { 141 | foreach ($values as $value) { 142 | if (! $typeChecker->validate($value, $type)) { 143 | return false; 144 | } 145 | } 146 | 147 | return true; 148 | } 149 | ); 150 | } 151 | } 152 | --------------------------------------------------------------------------------