diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 72dc1dac3..cdd1f7e24 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -15,7 +15,7 @@ jobs: strategy: fail-fast: false matrix: - php: [8.0] # Note: This workflow requires only the LATEST version of PHP + php: ['8.0'] # Note: This workflow requires only the LATEST version of PHP os: [ubuntu-latest] steps: @@ -62,7 +62,7 @@ jobs: strategy: fail-fast: false matrix: - php: [8.0] # Note: This workflow requires only the LATEST version of PHP + php: ['8.0'] # Note: This workflow requires only the LATEST version of PHP os: [ubuntu-latest] steps: @@ -112,7 +112,7 @@ jobs: strategy: fail-fast: false matrix: - php: [8.0] # Note: This workflow requires only the LATEST version of PHP + php: ['8.0'] # Note: This workflow requires only the LATEST version of PHP os: [ubuntu-latest] steps: @@ -162,7 +162,7 @@ jobs: strategy: fail-fast: false matrix: - php: ['7.2', '7.3', '7.4', '8.0'] + php: ['7.2', '7.3', '7.4', '8.0', '8.1'] os: [ubuntu-latest] stability: [prefer-lowest, prefer-stable] diff --git a/CHANGELOG.md b/CHANGELOG.md index 6b128cff0..0f057ed6e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,7 +6,12 @@ - **Medium Impact Changes** - Component `spiral/annotations` is deprecated. Use `spiral/attributes` instead - A minimal version of `doctrine/annotations` increased to `^1.12` + - [spiral/validation] Error messages for 'number::lower' and + 'number::higher' rules were changed to reflect that these checks are in + fact 'lower or equal' and 'higher or equal'. You may need to adjust + translations file accordingly. - **Other Features** + - [spiral/validation] Add array::count, array::range, array::shorter and array::longer rules (#435) - **Bug Fixes** ## v2.8.0 - 2021-06-03 diff --git a/composer.json b/composer.json index daba03684..679fbc960 100644 --- a/composer.json +++ b/composer.json @@ -27,6 +27,7 @@ "defuse/php-encryption": "^2.2", "doctrine/annotations": "^1.12", "doctrine/inflector": "^1.4|^2.0", + "laminas/laminas-diactoros": "^2.4", "league/flysystem": "^2.0", "monolog/monolog": "^2.2", "myclabs/deep-copy": "^1.9", @@ -39,7 +40,7 @@ "psr/log": "^1.0", "psr/simple-cache": ">=1.0", "spiral/composer-publish-plugin": "^1.0", - "symfony/console": "^5.1", + "symfony/console": "^5.3", "symfony/finder": "^5.1", "symfony/mailer": "^5.1", "symfony/polyfill-php73": "^1.22", @@ -127,13 +128,13 @@ "cycle/schema-builder": "^1.1", "guzzlehttp/psr7": "^1.7", "jetbrains/phpstorm-attributes": "^1.0", - "laminas/laminas-diactoros": "^2.3", + "laminas/laminas-hydrator": "^3.0|^4.0", "league/flysystem-async-aws-s3": "^2.0", "league/flysystem-aws-s3-v3": "^2.0", "mikey179/vfsstream": "^1.6", "mockery/mockery": "^1.3", "phpunit/phpunit": "^8.5|^9.0", - "ramsey/uuid": "^3.9", + "ramsey/uuid": "^4.2", "rector/rector": "0.12.15", "spiral/broadcast": "^2.0", "spiral/broadcast-ws": "^1.0", @@ -267,7 +268,7 @@ }, "extra": { "branch-alias": { - "dev-master": "2.8.x-dev" + "dev-master": "2.9.x-dev" } }, "config": { diff --git a/monorepo-builder.php b/monorepo-builder.php index 13fb6b057..56b930a3b 100644 --- a/monorepo-builder.php +++ b/monorepo-builder.php @@ -74,6 +74,7 @@ ], ], 'require' => [ + 'laminas/laminas-diactoros' => '^2.4', 'spiral/composer-publish-plugin' => '^1.0', ], 'autoload-dev' => [ @@ -87,14 +88,14 @@ 'mockery/mockery' => '^1.3', 'spiral/code-style' => '^1.0', 'spiral/database' => '^2.7.3', - 'spiral/migrations' => '^2.1', + 'spiral/migrations' => '^2.2', 'spiral/roadrunner' => '^1.9.2', 'spiral/php-grpc' => '^1.4', 'spiral/jobs' => '^2.2', 'spiral/broadcast' => '^2.0', 'spiral/broadcast-ws' => '^1.0', 'cycle/orm' => '^1.2.6', - 'laminas/laminas-hydrator' => '^3.0', + 'laminas/laminas-hydrator' => '^3.0|^4.0', 'cycle/annotated' => '^2.0.6', 'cycle/migrations' => '^1.0.1', 'cycle/proxy-factory' => '^1.2', diff --git a/src/AnnotatedRoutes/composer.json b/src/AnnotatedRoutes/composer.json index fbc1a99b1..f264e15de 100644 --- a/src/AnnotatedRoutes/composer.json +++ b/src/AnnotatedRoutes/composer.json @@ -16,13 +16,14 @@ ], "require": { "php": ">=7.2", - "spiral/router": "^2.8", - "spiral/annotations": "^2.8" + "spiral/annotations": "^2.9", + "spiral/attributes": "^2.9", + "spiral/router": "^2.9" }, "require-dev": { "phpunit/phpunit": "^8.5|^9.0", "mockery/mockery": "^1.3", - "laminas/laminas-diactoros": "^2.3" + "laminas/laminas-diactoros": "^2.4" }, "autoload": { "psr-4": { @@ -36,7 +37,7 @@ }, "extra": { "branch-alias": { - "dev-master": "2.8.x-dev" + "dev-master": "2.9.x-dev" } }, "config": { diff --git a/src/AnnotatedRoutes/src/Annotation/Route.php b/src/AnnotatedRoutes/src/Annotation/Route.php index 14599b548..a210804ed 100644 --- a/src/AnnotatedRoutes/src/Annotation/Route.php +++ b/src/AnnotatedRoutes/src/Annotation/Route.php @@ -22,11 +22,12 @@ * @Target({"METHOD"}) * @Attributes({ * @Attribute("route", required=true, type="string"), - * @Attribute("name", required=true, type="string"), + * @Attribute("name", type="string"), * @Attribute("verbs", required=true, type="mixed"), * @Attribute("defaults", type="array"), * @Attribute("group", type="string"), - * @Attribute("middleware", type="array") + * @Attribute("middleware", type="array"), + * @Attribute("priority", type="int") * }) */ #[\Attribute(\Attribute::TARGET_METHOD), NamedArgumentConstructor] @@ -40,7 +41,7 @@ final class Route public $route; /** - * @var string + * @var null|string */ public $name; @@ -70,19 +71,25 @@ final class Route */ public $middleware = []; + /** + * @var int + */ + public $priority; + /** * @psalm-param non-empty-string $route - * @psalm-param non-empty-string $name + * @psalm-param non-empty-string|null $name * @psalm-param non-empty-string|array $methods * @psalm-param non-empty-string $group */ public function __construct( string $route, - string $name, + string $name = null, $methods = \Spiral\Router\Route::VERBS, array $defaults = [], string $group = self::DEFAULT_GROUP, - array $middleware = [] + array $middleware = [], + int $priority = 0 ) { $this->route = $route; $this->name = $name; @@ -90,5 +97,6 @@ public function __construct( $this->defaults = $defaults; $this->group = $group; $this->middleware = $middleware; + $this->priority = $priority; } } diff --git a/src/AnnotatedRoutes/src/GroupRegistry.php b/src/AnnotatedRoutes/src/GroupRegistry.php index 13c263461..7991374a2 100644 --- a/src/AnnotatedRoutes/src/GroupRegistry.php +++ b/src/AnnotatedRoutes/src/GroupRegistry.php @@ -42,7 +42,7 @@ public function getGroup(string $name): RouteGroup /** * @return RouteGroup[]|\ArrayIterator */ - public function getIterator() + public function getIterator(): \Traversable { return new \ArrayIterator($this->groups); } diff --git a/src/AnnotatedRoutes/src/RouteLocator.php b/src/AnnotatedRoutes/src/RouteLocator.php index 240cbf46b..41e13d30f 100644 --- a/src/AnnotatedRoutes/src/RouteLocator.php +++ b/src/AnnotatedRoutes/src/RouteLocator.php @@ -40,6 +40,7 @@ public function findDeclarations(): array continue; } + $route->name = $route->name ?? $this->generateName($route); $result[$route->name] = [ 'pattern' => $route->route, 'controller' => $class->getName(), @@ -48,10 +49,27 @@ public function findDeclarations(): array 'verbs' => (array) $route->methods, 'defaults' => $route->defaults, 'middleware' => (array) $route->middleware, + 'priority' => $route->priority, ]; } } + \uasort($result, static function (array $route1, array $route2) { + return $route1['priority'] <=> $route2['priority']; + }); + return $result; } + + /** + * Generates route name based on declared methods and route. + */ + private function generateName(Route $route): string + { + $methods = \is_array($route->methods) + ? \implode(',', $route->methods) + : $route->methods; + + return \mb_strtolower(\sprintf('%s:%s', $methods, $route->route)); + } } diff --git a/src/AnnotatedRoutes/tests/App/Controller/HomeController.php b/src/AnnotatedRoutes/tests/App/Controller/HomeController.php index a7cf83ed8..f44e53a8b 100644 --- a/src/AnnotatedRoutes/tests/App/Controller/HomeController.php +++ b/src/AnnotatedRoutes/tests/App/Controller/HomeController.php @@ -30,4 +30,10 @@ public function method() { return 'method'; } + + #[Route(route: '/attribute', name: 'attribute', methods: 'GET', group: 'test')] + public function attribute() + { + return 'attribute'; + } } diff --git a/src/AnnotatedRoutes/tests/App/Controller/NamelessRoutesController.php b/src/AnnotatedRoutes/tests/App/Controller/NamelessRoutesController.php new file mode 100644 index 000000000..f864284c2 --- /dev/null +++ b/src/AnnotatedRoutes/tests/App/Controller/NamelessRoutesController.php @@ -0,0 +1,41 @@ +", name="page_get", methods="GET") + */ + public function get($page) + { + return 'page-'.$page; + } + + /** + * @Route("/page/about", name="page_about", methods="GET", priority=-1) + */ + public function about() + { + return 'about'; + } +} diff --git a/src/AnnotatedRoutes/tests/App/runtime/cache/routes.php b/src/AnnotatedRoutes/tests/App/runtime/cache/routes.php deleted file mode 100644 index 8e14179db..000000000 --- a/src/AnnotatedRoutes/tests/App/runtime/cache/routes.php +++ /dev/null @@ -1,36 +0,0 @@ - - array ( - 'pattern' => '/', - 'controller' => 'Spiral\\Tests\\Router\\App\\Controller\\HomeController', - 'action' => 'index', - 'group' => 'default', - 'verbs' => - array ( - 0 => 'GET', - ), - 'defaults' => - array ( - ), - 'middleware' => - array ( - ), - ), - 'method' => - array ( - 'pattern' => '/', - 'controller' => 'Spiral\\Tests\\Router\\App\\Controller\\HomeController', - 'action' => 'method', - 'group' => 'default', - 'verbs' => - array ( - 0 => 'POST', - ), - 'defaults' => - array ( - ), - 'middleware' => - array ( - ), - ), -); \ No newline at end of file diff --git a/src/AnnotatedRoutes/tests/CacheTest.php b/src/AnnotatedRoutes/tests/CacheTest.php deleted file mode 100644 index b5faa2931..000000000 --- a/src/AnnotatedRoutes/tests/CacheTest.php +++ /dev/null @@ -1,53 +0,0 @@ -app = $this->makeApp(['DEBUG' => false]); - } - - public function testCache(): void - { - $cache = __DIR__ . '/App/runtime/cache/routes.php'; - $this->assertFileExists($cache); - - $this->assertIsIterable($this->include($cache)); - $this->assertCount(2, $this->include($cache)); - - - $cli = $this->app->getConsole(); - $cli->run('route:reset'); - - $this->assertNull($this->include($cache)); - } - - /** - * @param string $file - * @return mixed - */ - private function include(string $file) - { - // Required when "opcache.cli_enabled=1" - if (\function_exists('\\opcache_invalidate')) { - \opcache_invalidate($file); - } - - return require $file; - } -} diff --git a/src/AnnotatedRoutes/tests/IntegrationTest.php b/src/AnnotatedRoutes/tests/IntegrationTest.php index 553670b0f..f16d83ebb 100644 --- a/src/AnnotatedRoutes/tests/IntegrationTest.php +++ b/src/AnnotatedRoutes/tests/IntegrationTest.php @@ -30,12 +30,44 @@ public function testRoute(): void $this->assertStringContainsString('index', $r->getBody()->__toString()); } + public function testAttributeRoute(): void + { + $r = $this->get('/attribute'); + $this->assertStringContainsString('attribute', $r->getBody()->__toString()); + } + public function testRoute2(): void { $r = $this->post('/'); $this->assertStringContainsString('method', $r->getBody()->__toString()); } + public function testRoute3(): void + { + $r = $this->get('/page/test'); + + $this->assertSame('page-test', $r->getBody()->__toString()); + } + + public function testRoute4(): void + { + $r = $this->get('/page/about'); + + $this->assertSame('about', $r->getBody()->__toString()); + } + + public function testRoutesWithoutNames(): void + { + $r = $this->get('/nameless'); + $this->assertSame('index', $r->getBody()->__toString()); + + $r = $this->post('/nameless'); + $this->assertSame('method', $r->getBody()->__toString()); + + $r = $this->get('/nameless/route'); + $this->assertSame('route', $r->getBody()->__toString()); + } + public function get( $uri, array $query = [], diff --git a/src/Annotations/composer.json b/src/Annotations/composer.json index 8cd3d13ad..f8bc26764 100644 --- a/src/Annotations/composer.json +++ b/src/Annotations/composer.json @@ -10,8 +10,8 @@ }, "require": { "php": ">=7.2", - "spiral/tokenizer": "^2.8", - "doctrine/annotations": "^1.11" + "spiral/attributes": "^2.9", + "spiral/tokenizer": "^2.9" }, "require-dev": { "phpunit/phpunit": "^8.5|^9.0" @@ -28,7 +28,7 @@ }, "extra": { "branch-alias": { - "dev-master": "2.8.x-dev" + "dev-master": "2.9.x-dev" } }, "config": { diff --git a/src/Annotations/src/AnnotationLocator.php b/src/Annotations/src/AnnotationLocator.php index a1d295641..2853a765b 100644 --- a/src/Annotations/src/AnnotationLocator.php +++ b/src/Annotations/src/AnnotationLocator.php @@ -11,7 +11,8 @@ namespace Spiral\Annotations; -use Doctrine\Common\Annotations\AnnotationReader; +use Spiral\Attributes\AnnotationReader; +use Spiral\Attributes\Factory; use Spiral\Attributes\ReaderInterface; use Spiral\Core\Container\SingletonInterface; use Spiral\Tokenizer\ClassesInterface; @@ -26,7 +27,7 @@ final class AnnotationLocator implements SingletonInterface /** @var ClassesInterface */ private $classLocator; - /** @var AnnotationReader */ + /** @var ReaderInterface */ private $reader; /** @var array */ @@ -35,13 +36,13 @@ final class AnnotationLocator implements SingletonInterface /** * AnnotationLocator constructor. * - * @param AnnotationReader|null $reader + * @param ReaderInterface|null $reader * @throws \Doctrine\Common\Annotations\AnnotationException */ - public function __construct(ClassesInterface $classLocator, AnnotationReader $reader = null) + public function __construct(ClassesInterface $classLocator, ReaderInterface $reader = null) { $this->classLocator = $classLocator; - $this->reader = $reader ?? new AnnotationReader(); + $this->reader = $reader ?? (new Factory())->create(); } /** @@ -63,7 +64,7 @@ public function withTargets(array $targets): self public function findClasses(string $annotation): iterable { foreach ($this->getTargets() as $target) { - $found = $this->reader->getClassAnnotation($target, $annotation); + $found = $this->reader->firstClassMetadata($target, $annotation); if ($found !== null) { yield new AnnotatedClass($target, $found); } @@ -79,7 +80,7 @@ public function findMethods(string $annotation): iterable { foreach ($this->getTargets() as $target) { foreach ($target->getMethods() as $method) { - $found = $this->reader->getMethodAnnotation($method, $annotation); + $found = $this->reader->firstFunctionMetadata($method, $annotation); if ($found !== null) { yield new AnnotatedMethod($method, $found); } @@ -96,7 +97,7 @@ public function findProperties(string $annotation): iterable { foreach ($this->getTargets() as $target) { foreach ($target->getProperties() as $property) { - $found = $this->reader->getPropertyAnnotation($property, $annotation); + $found = $this->reader->firstPropertyMetadata($property, $annotation); if ($found !== null) { yield new AnnotatedProperty($property, $found); } diff --git a/src/Annotations/tests/Fixtures/Annotation/Value.php b/src/Annotations/tests/Fixtures/Annotation/ClassAnnotation.php similarity index 76% rename from src/Annotations/tests/Fixtures/Annotation/Value.php rename to src/Annotations/tests/Fixtures/Annotation/ClassAnnotation.php index cd76c7d19..cb7c0c1fa 100644 --- a/src/Annotations/tests/Fixtures/Annotation/Value.php +++ b/src/Annotations/tests/Fixtures/Annotation/ClassAnnotation.php @@ -11,10 +11,13 @@ namespace Spiral\Tests\Annotations\Fixtures\Annotation; +use Attribute; + /** * @Annotation */ -class Value +#[Attribute(Attribute::TARGET_CLASS)] +class ClassAnnotation { /** @var string */ public $value; diff --git a/src/Annotations/tests/Fixtures/Annotation/Route.php b/src/Annotations/tests/Fixtures/Annotation/MethodAnnotation.php similarity index 75% rename from src/Annotations/tests/Fixtures/Annotation/Route.php rename to src/Annotations/tests/Fixtures/Annotation/MethodAnnotation.php index a6756b1a8..4ce79666a 100644 --- a/src/Annotations/tests/Fixtures/Annotation/Route.php +++ b/src/Annotations/tests/Fixtures/Annotation/MethodAnnotation.php @@ -11,10 +11,13 @@ namespace Spiral\Tests\Annotations\Fixtures\Annotation; +use Attribute; + /** * @Annotation */ -class Route +#[Attribute(Attribute::TARGET_METHOD)] +class MethodAnnotation { /** @var string */ public $path; diff --git a/src/Annotations/tests/Fixtures/Annotation/Another.php b/src/Annotations/tests/Fixtures/Annotation/PropertyAnnotation.php similarity index 74% rename from src/Annotations/tests/Fixtures/Annotation/Another.php rename to src/Annotations/tests/Fixtures/Annotation/PropertyAnnotation.php index bc44f40cc..fd26e399f 100644 --- a/src/Annotations/tests/Fixtures/Annotation/Another.php +++ b/src/Annotations/tests/Fixtures/Annotation/PropertyAnnotation.php @@ -11,10 +11,13 @@ namespace Spiral\Tests\Annotations\Fixtures\Annotation; +use Attribute; + /** * @Annotation */ -class Another +#[Attribute(Attribute::TARGET_PROPERTY)] +class PropertyAnnotation { /** @var string */ public $id; diff --git a/src/Annotations/tests/Fixtures/AttributeTestClass.php b/src/Annotations/tests/Fixtures/AttributeTestClass.php new file mode 100644 index 000000000..d5d5cac57 --- /dev/null +++ b/src/Annotations/tests/Fixtures/AttributeTestClass.php @@ -0,0 +1,28 @@ +getLocator(__DIR__ . '/Fixtures')->findClasses(Value::class); + // Annotations. + $classes = $this->getAnnotationsLocator(__DIR__ . '/Fixtures')->findClasses(ClassAnnotation::class); $classes = iterator_to_array($classes); $this->assertCount(1, $classes); @@ -34,11 +38,23 @@ public function testLocateClasses() $this->assertSame(TestClass::class, $class->getClass()->getName()); $this->assertSame('abc', $class->getAnnotation()->value); } + + // Attributes. + $classes = $this->getAttributesLocator(__DIR__ . '/Fixtures')->findClasses(ClassAnnotation::class); + $classes = iterator_to_array($classes); + + $this->assertCount(1, $classes); + + foreach ($classes as $class) { + $this->assertSame(AttributeTestClass::class, $class->getClass()->getName()); + $this->assertSame('abc', $class->getAnnotation()->value); + } } public function testLocateProperties() { - $props = $this->getLocator(__DIR__ . '/Fixtures')->findProperties(Another::class); + // Annotations. + $props = $this->getAnnotationsLocator(__DIR__ . '/Fixtures')->findProperties(PropertyAnnotation::class); $props = iterator_to_array($props); $this->assertCount(1, $props); @@ -48,11 +64,24 @@ public function testLocateProperties() $this->assertSame('name', $prop->getProperty()->getName()); $this->assertSame('123', $prop->getAnnotation()->id); } + + // Attributes. + $props = $this->getAttributesLocator(__DIR__ . '/Fixtures')->findProperties(PropertyAnnotation::class); + $props = iterator_to_array($props); + + $this->assertCount(1, $props); + + foreach ($props as $prop) { + $this->assertSame(AttributeTestClass::class, $prop->getClass()->getName()); + $this->assertSame('name', $prop->getProperty()->getName()); + $this->assertSame('123', $prop->getAnnotation()->id); + } } public function testLocateMethods() { - $methods = $this->getLocator(__DIR__ . '/Fixtures')->findMethods(Route::class); + // Annotations. + $methods = $this->getAnnotationsLocator(__DIR__ . '/Fixtures')->findMethods(MethodAnnotation::class); $methods = iterator_to_array($methods); $this->assertCount(1, $methods); @@ -62,14 +91,35 @@ public function testLocateMethods() $this->assertSame('testMethod', $m->getMethod()->getName()); $this->assertSame('/', $m->getAnnotation()->path); } + + // Attributes. + $methods = $this->getAttributesLocator(__DIR__ . '/Fixtures')->findMethods(MethodAnnotation::class); + $methods = iterator_to_array($methods); + + $this->assertCount(1, $methods); + + foreach ($methods as $m) { + $this->assertSame(AttributeTestClass::class, $m->getClass()->getName()); + $this->assertSame('testMethod', $m->getMethod()->getName()); + $this->assertSame('/', $m->getAnnotation()->path); + } } - private function getLocator(string $directory): AnnotationLocator + private function getAnnotationsLocator(string $directory): AnnotationLocator { AnnotationRegistry::registerLoader('class_exists'); return new AnnotationLocator( - (new ClassLocator((new Finder())->files()->in([$directory]))) + (new ClassLocator((new Finder())->files()->in([$directory]))), + new AnnotationReader() + ); + } + + private function getAttributesLocator(string $directory): AnnotationLocator + { + return new AnnotationLocator( + (new ClassLocator((new Finder())->files()->in([$directory]))), + new AttributeReader() ); } } diff --git a/src/Attributes/composer.json b/src/Attributes/composer.json index 1a63a65ec..64bdc9820 100644 --- a/src/Attributes/composer.json +++ b/src/Attributes/composer.json @@ -21,7 +21,7 @@ "nikic/php-parser": "^4.1" }, "require-dev": { - "doctrine/annotations": "^1.11", + "doctrine/annotations": "^1.12", "jetbrains/phpstorm-attributes": "^1.0", "symfony/var-dumper": "^5.2", "phpunit/phpunit": "^8.5|^9.0" @@ -41,7 +41,7 @@ }, "extra": { "branch-alias": { - "dev-master": "2.8.x-dev" + "dev-master": "2.9.x-dev" } }, "scripts": { diff --git a/src/Auth/composer.json b/src/Auth/composer.json index dcb9f03f6..ee22eddea 100644 --- a/src/Auth/composer.json +++ b/src/Auth/composer.json @@ -32,7 +32,7 @@ }, "extra": { "branch-alias": { - "dev-master": "2.8.x-dev" + "dev-master": "2.9.x-dev" } }, "config": { diff --git a/src/AuthHttp/composer.json b/src/AuthHttp/composer.json index e6162a678..1ad75d231 100644 --- a/src/AuthHttp/composer.json +++ b/src/AuthHttp/composer.json @@ -16,16 +16,16 @@ "require": { "php": ">=7.2", "ext-json": "*", - "spiral/auth": "^2.8", + "spiral/auth": "^2.9", "psr/http-message": "^1.0", "psr/http-server-middleware": "^1.0" }, "require-dev": { "phpunit/phpunit": "^8.5|^9.0", - "spiral/cookies": "^2.8", - "spiral/http": "^2.8", - "spiral/debug": "^2.8", - "laminas/laminas-diactoros": "^2.3" + "spiral/cookies": "^2.9", + "spiral/http": "^2.9", + "spiral/debug": "^2.9", + "laminas/laminas-diactoros": "^2.4" }, "autoload": { "psr-4": { @@ -39,7 +39,7 @@ }, "extra": { "branch-alias": { - "dev-master": "2.8.x-dev" + "dev-master": "2.9.x-dev" } }, "config": { diff --git a/src/AuthHttp/src/Transport/HeaderTransport.php b/src/AuthHttp/src/Transport/HeaderTransport.php index 1a1023883..2f1ee2d65 100644 --- a/src/AuthHttp/src/Transport/HeaderTransport.php +++ b/src/AuthHttp/src/Transport/HeaderTransport.php @@ -23,9 +23,13 @@ final class HeaderTransport implements HttpTransportInterface /** @var string */ private $header; - public function __construct(string $header = 'X-Auth-Token') + /** @var string */ + private $valueFormat; + + public function __construct(string $header = 'X-Auth-Token', string $valueFormat = '%s') { $this->header = $header; + $this->valueFormat = $valueFormat; } /** @@ -34,7 +38,7 @@ public function __construct(string $header = 'X-Auth-Token') public function fetchToken(Request $request): ?string { if ($request->hasHeader($this->header)) { - return $request->getHeaderLine($this->header); + return $this->extractToken($request); } return null; @@ -49,11 +53,11 @@ public function commitToken( string $tokenID, \DateTimeInterface $expiresAt = null ): Response { - if ($request->hasHeader($this->header) && $request->getHeaderLine($this->header) === $tokenID) { + if ($request->hasHeader($this->header) && $this->extractToken($request) === $tokenID) { return $response; } - return $response->withAddedHeader($this->header, $tokenID); + return $response->withAddedHeader($this->header, sprintf($this->valueFormat, $tokenID)); } /** @@ -63,4 +67,17 @@ public function removeToken(Request $request, Response $response, string $tokenI { return $response; } + + private function extractToken(Request $request): ?string + { + $headerLine = $request->getHeaderLine($this->header); + + if ($this->valueFormat !== '%s') { + [$token] = sscanf($headerLine, $this->valueFormat); + + return $token !== null ? (string) $token : null; + } + + return $headerLine; + } } diff --git a/src/AuthHttp/tests/HeaderTransportTest.php b/src/AuthHttp/tests/HeaderTransportTest.php index d2b12601d..fa4b41390 100644 --- a/src/AuthHttp/tests/HeaderTransportTest.php +++ b/src/AuthHttp/tests/HeaderTransportTest.php @@ -61,6 +61,29 @@ static function (ServerRequestInterface $request, ResponseInterface $response): self::assertSame('good-token:{"id":"good-token"}', (string)$response->getBody()); } + public function testHeaderTokenWithCustomValueFormat(): void + { + $http = $this->getCore(new HeaderTransport('Authorization', 'Bearer %s')); + + $http->setHandler( + static function (ServerRequestInterface $request, ResponseInterface $response): void { + if ($request->getAttribute('authContext')->getToken() === null) { + echo 'no token'; + } else { + echo $request->getAttribute('authContext')->getToken()->getID(); + echo ':'; + echo json_encode($request->getAttribute('authContext')->getToken()->getPayload()); + } + } + ); + + $response = $http->handle(new ServerRequest([], [], null, 'GET', 'php://input', [ + 'Authorization' => 'Bearer good-token' + ])); + + self::assertSame(['text/html; charset=UTF-8'], $response->getHeader('Content-Type')); + self::assertSame('good-token:{"id":"good-token"}', (string)$response->getBody()); + } public function testBadHeaderToken(): void { $http = $this->getCore(new HeaderTransport()); @@ -120,6 +143,23 @@ static function (ServerRequestInterface $request, ResponseInterface $response): self::assertSame(['new-token'], $response->getHeader('X-Auth-Token')); } + public function testCommitTokenWithCustomValueFormat(): void + { + $http = $this->getCore(new HeaderTransport('Authorization', 'Bearer %s')); + + $http->setHandler( + static function (ServerRequestInterface $request, ResponseInterface $response): void { + $request->getAttribute('authContext')->start( + new TestAuthHttpToken('new-token', ['ok' => 1]) + ); + } + ); + + $response = $http->handle(new ServerRequest([], [], null, 'GET', 'php://input', [])); + + self::assertSame(['Bearer new-token'], $response->getHeader('Authorization')); + } + protected function getCore(HttpTransportInterface $transport): Http { $config = new HttpConfig([ diff --git a/src/Boot/composer.json b/src/Boot/composer.json index ee1045f04..c31eab736 100644 --- a/src/Boot/composer.json +++ b/src/Boot/composer.json @@ -16,11 +16,11 @@ ], "require": { "php": ">=7.2", - "spiral/core": "^2.8", - "spiral/files": "^2.8", - "spiral/config": "^2.8", - "spiral/debug": "^2.8", - "spiral/exceptions": "^2.8" + "spiral/core": "^2.9", + "spiral/files": "^2.9", + "spiral/config": "^2.9", + "spiral/debug": "^2.9", + "spiral/exceptions": "^2.9" }, "require-dev": { "phpunit/phpunit": "^8.5|^9.0", @@ -41,7 +41,7 @@ }, "extra": { "branch-alias": { - "dev-master": "2.8.x-dev" + "dev-master": "2.9.x-dev" } }, "config": { diff --git a/src/Bridge/DataGrid/composer.json b/src/Bridge/DataGrid/composer.json index a2c816f6a..87bec998e 100644 --- a/src/Bridge/DataGrid/composer.json +++ b/src/Bridge/DataGrid/composer.json @@ -17,15 +17,15 @@ "require": { "php": ">=7.2", "ext-json": "*", - "spiral/boot": "^2.8", - "spiral/http": "^2.8", - "spiral/data-grid": "^2.8" + "spiral/attributes": "^2.9", + "spiral/boot": "^2.9", + "spiral/data-grid": "^2.9", + "spiral/http": "^2.9" }, "require-dev": { "phpunit/phpunit": "^8.5|^9.0", "cycle/orm": "^1.2.6", - "spiral/hmvc": "^2.8", - "doctrine/annotations": "^1.11" + "spiral/hmvc": "^2.9" }, "autoload": { "psr-4": { @@ -39,7 +39,7 @@ }, "extra": { "branch-alias": { - "dev-master": "2.8.x-dev" + "dev-master": "2.9.x-dev" } }, "config": { diff --git a/src/Bridge/DataGrid/src/Annotation/DataGrid.php b/src/Bridge/DataGrid/src/Annotation/DataGrid.php index fede1ae46..4b41d25dd 100644 --- a/src/Bridge/DataGrid/src/Annotation/DataGrid.php +++ b/src/Bridge/DataGrid/src/Annotation/DataGrid.php @@ -11,36 +11,36 @@ namespace Spiral\DataGrid\Annotation; -use Doctrine\Common\Annotations\Annotation\Attribute; -use Doctrine\Common\Annotations\Annotation\Attributes; +use Attribute; +use Doctrine\Common\Annotations\Annotation; use Spiral\Attributes\NamedArgumentConstructor; /** * @Annotation * @NamedArgumentConstructor * @Target({"METHOD"}) - * @Attributes({ - * @Attribute("grid", required=true, type="string"), - * @Attribute("view", type="string"), - * @Attribute("defaults", type="array"), - * @Attribute("options", type="array"), - * @Attribute("factory", type="string") + * @Annotation\Attributes({ + * @Annotation\Attribute("grid", required=true, type="string"), + * @Annotation\Attribute("view", type="string"), + * @Annotation\Attribute("defaults", type="array"), + * @Annotation\Attribute("options", type="array"), + * @Annotation\Attribute("factory", type="string") * }) */ -#[\Attribute(\Attribute::TARGET_METHOD), NamedArgumentConstructor] +#[Attribute(Attribute::TARGET_METHOD), NamedArgumentConstructor] class DataGrid { /** * Points to grid schema. * - * @type string + * @var string */ public $grid; /** * Response options, default to GridSchema->__invoke() if such method exists. * - * @var string + * @var string|null */ public $view; @@ -49,19 +49,19 @@ class DataGrid * * @var array */ - public $defaults = []; + public $defaults; /** * Response options, default to GridSchema->getOptions() if such method exists. * * @var array */ - public $options = []; + public $options; /** * Custom user GridFactory * - * @var string + * @var string|null */ public $factory; diff --git a/src/Bridge/DataGrid/src/Interceptor/GridInterceptor.php b/src/Bridge/DataGrid/src/Interceptor/GridInterceptor.php index 429819f8f..707df2e90 100644 --- a/src/Bridge/DataGrid/src/Interceptor/GridInterceptor.php +++ b/src/Bridge/DataGrid/src/Interceptor/GridInterceptor.php @@ -109,6 +109,7 @@ private function getSchema(string $controller, string $action): ?array return null; } + /** @var null|DataGrid $dataGrid */ $dataGrid = $this->reader->firstFunctionMetadata($method, DataGrid::class); if ($dataGrid === null) { return null; diff --git a/src/Bridge/Dotenv/composer.json b/src/Bridge/Dotenv/composer.json index af886669e..a46ede43b 100644 --- a/src/Bridge/Dotenv/composer.json +++ b/src/Bridge/Dotenv/composer.json @@ -16,7 +16,7 @@ ], "require": { "php": ">=7.2", - "spiral/boot": "^2.8", + "spiral/boot": "^2.9", "vlucas/phpdotenv": "^5.4" }, "require-dev": { @@ -34,7 +34,7 @@ }, "extra": { "branch-alias": { - "dev-master": "2.8.x-dev" + "dev-master": "2.9.x-dev" } }, "config": { diff --git a/src/Bridge/Monolog/composer.json b/src/Bridge/Monolog/composer.json index df08b4ff1..81c9182d8 100644 --- a/src/Bridge/Monolog/composer.json +++ b/src/Bridge/Monolog/composer.json @@ -16,7 +16,7 @@ ], "require": { "php": ">=7.2", - "spiral/boot": "^2.8", + "spiral/boot": "^2.9", "monolog/monolog": "^2.2" }, "require-dev": { @@ -35,7 +35,7 @@ }, "extra": { "branch-alias": { - "dev-master": "2.8.x-dev" + "dev-master": "2.9.x-dev" } }, "config": { diff --git a/src/Bridge/Stempler/composer.json b/src/Bridge/Stempler/composer.json index abdd89a58..f9b93c6cb 100644 --- a/src/Bridge/Stempler/composer.json +++ b/src/Bridge/Stempler/composer.json @@ -16,19 +16,19 @@ ], "require": { "php": ">=7.2", - "spiral/stempler": "^2.8", - "spiral/boot": "^2.8", - "spiral/files": "^2.8", - "spiral/config": "^2.8", - "spiral/translator": "^2.8", - "spiral/router": "^2.8", - "spiral/views": "^2.8", - "spiral/core": "^2.8" + "spiral/stempler": "^2.9", + "spiral/boot": "^2.9", + "spiral/files": "^2.9", + "spiral/config": "^2.9", + "spiral/translator": "^2.9", + "spiral/router": "^2.9", + "spiral/views": "^2.9", + "spiral/core": "^2.9" }, "require-dev": { "phpunit/phpunit": "^8.5|^9.0", "mockery/mockery": "^1.3", - "spiral/dumper": "^2.8" + "spiral/dumper": "^2.9" }, "autoload": { "psr-4": { @@ -42,7 +42,7 @@ }, "extra": { "branch-alias": { - "dev-master": "2.8.x-dev" + "dev-master": "2.9.x-dev" } }, "config": { diff --git a/src/Config/composer.json b/src/Config/composer.json index b84301874..c080dc5ce 100644 --- a/src/Config/composer.json +++ b/src/Config/composer.json @@ -17,7 +17,7 @@ "require": { "php": ">=7.2", "ext-json": "*", - "spiral/core": "^2.8" + "spiral/core": "^2.9" }, "require-dev": { "phpunit/phpunit": "^8.5|^9.0", @@ -35,7 +35,7 @@ }, "extra": { "branch-alias": { - "dev-master": "2.8.x-dev" + "dev-master": "2.9.x-dev" } }, "config": { diff --git a/src/Config/src/Loader/DirectoryLoader.php b/src/Config/src/Loader/DirectoryLoader.php index 5912135c5..14513a685 100644 --- a/src/Config/src/Loader/DirectoryLoader.php +++ b/src/Config/src/Loader/DirectoryLoader.php @@ -57,11 +57,11 @@ public function load(string $section): array try { return $this->getLoader($extension)->loadFile($section, $filename); } catch (LoaderException $e) { - throw new LoaderException("Unable to load config `{$section}`.", $e->getCode(), $e); + throw new LoaderException("Unable to load config `{$section}`: {$e->getMessage()}", $e->getCode(), $e); } } - throw new LoaderException("Unable to load config `{$section}`."); + throw new LoaderException("Unable to load config `{$section}`: no suitable loader found."); } private function loaderExtensions(): array diff --git a/src/Console/composer.json b/src/Console/composer.json index d5577636c..2516dad50 100644 --- a/src/Console/composer.json +++ b/src/Console/composer.json @@ -16,8 +16,8 @@ ], "require": { "php": ">=7.2", - "spiral/core": "^2.8", - "symfony/console": "^5.1" + "spiral/core": "^2.9", + "symfony/console": "^5.3" }, "require-dev": { "phpunit/phpunit": "^8.5|^9.0", @@ -35,7 +35,7 @@ }, "extra": { "branch-alias": { - "dev-master": "2.8.x-dev" + "dev-master": "2.9.x-dev" } }, "config": { diff --git a/src/Console/src/Command.php b/src/Console/src/Command.php index 76e76eb24..2bd06b16b 100644 --- a/src/Console/src/Command.php +++ b/src/Console/src/Command.php @@ -41,7 +41,7 @@ abstract class Command extends SymfonyCommand // getArguments() method. protected const ARGUMENTS = []; - /** @var Container */ + /** @var Container|null */ protected $container; public function setContainer(ContainerInterface $container): void diff --git a/src/Console/src/StaticLocator.php b/src/Console/src/StaticLocator.php index 9df60a4b2..4d69b2b9c 100644 --- a/src/Console/src/StaticLocator.php +++ b/src/Console/src/StaticLocator.php @@ -12,20 +12,23 @@ namespace Spiral\Console; use Psr\Container\ContainerInterface; +use Spiral\Console\Traits\LazyTrait; use Spiral\Core\Container; final class StaticLocator implements LocatorInterface { - /** @var []string */ + use LazyTrait; + + /** @var string[] */ private $commands; /** @var ContainerInterface */ - private $factory; + private $container; public function __construct(array $commands, ContainerInterface $container = null) { $this->commands = $commands; - $this->factory = $container ?? new Container(); + $this->container = $container ?? new Container(); } /** @@ -35,7 +38,9 @@ public function locateCommands(): array { $commands = []; foreach ($this->commands as $command) { - $commands[] = $this->factory->get($command); + $commands[] = $this->supportsLazyLoading($command) + ? $this->createLazyCommand($command) + : $this->container->get($command); } return $commands; diff --git a/src/Console/src/Traits/HelpersTrait.php b/src/Console/src/Traits/HelpersTrait.php index 7e1fdb35d..80eea2823 100644 --- a/src/Console/src/Traits/HelpersTrait.php +++ b/src/Console/src/Traits/HelpersTrait.php @@ -24,7 +24,7 @@ trait HelpersTrait * OutputInterface is the interface implemented by all Output classes. Only exists when command * are being executed. * - * @var OutputInterface + * @var OutputInterface|null */ protected $output; @@ -32,7 +32,7 @@ trait HelpersTrait * InputInterface is the interface implemented by all input classes. Only exists when command * are being executed. * - * @var InputInterface + * @var InputInterface|null */ protected $input; diff --git a/src/Console/src/Traits/LazyTrait.php b/src/Console/src/Traits/LazyTrait.php new file mode 100644 index 000000000..3424dbcbb --- /dev/null +++ b/src/Console/src/Traits/LazyTrait.php @@ -0,0 +1,57 @@ +container->get($class); + $command->setContainer($this->container); + + return $command; + } + ); + } +} diff --git a/src/Console/tests/Fixtures/LazyLoadedCommand.php b/src/Console/tests/Fixtures/LazyLoadedCommand.php new file mode 100644 index 000000000..c741a14ae --- /dev/null +++ b/src/Console/tests/Fixtures/LazyLoadedCommand.php @@ -0,0 +1,27 @@ +write('OK'); + + return self::SUCCESS; + } +} diff --git a/src/Console/tests/LazyTest.php b/src/Console/tests/LazyTest.php new file mode 100644 index 000000000..9ec51af16 --- /dev/null +++ b/src/Console/tests/LazyTest.php @@ -0,0 +1,59 @@ +container + ); + $commands = $locator->locateCommands(); + $command = reset($commands); + + $this->assertInstanceOf(LazyCommand::class, $command); + $this->assertSame('lazy', $command->getName()); + $this->assertSame('Lazy description', $command->getDescription()); + } + + public function testLazyCommandCreationInStaticLocator(): void + { + $locator = new StaticLocator([LazyLoadedCommand::class]); + $commands = $locator->locateCommands(); + $command = reset($commands); + + $this->assertInstanceOf(LazyCommand::class, $command); + $this->assertSame('lazy', $command->getName()); + $this->assertSame('Lazy description', $command->getDescription()); + } + + public function testLazyCommandExecution(): void + { + $core = $this->getCore(new StaticLocator([LazyLoadedCommand::class])); + $output = $core->run('lazy'); + $this->assertSame('OK', $output->getOutput()->fetch()); + } +} diff --git a/src/Cookies/composer.json b/src/Cookies/composer.json index 9365937ec..2c4ef9e29 100644 --- a/src/Cookies/composer.json +++ b/src/Cookies/composer.json @@ -16,14 +16,14 @@ ], "require": { "php": ">=7.2", - "spiral/encrypter": "^2.8", + "spiral/encrypter": "^2.9", "psr/http-server-middleware": "^1.0" }, "require-dev": { "phpunit/phpunit": "^8.5|^9.0", "mockery/mockery": "^1.3", - "spiral/http": "^2.8", - "laminas/laminas-diactoros": "^2.3" + "spiral/http": "^2.9", + "laminas/laminas-diactoros": "^2.4" }, "autoload": { "psr-4": { @@ -37,7 +37,7 @@ }, "extra": { "branch-alias": { - "dev-master": "2.8.x-dev" + "dev-master": "2.9.x-dev" } }, "config": { diff --git a/src/Core/composer.json b/src/Core/composer.json index 7f4c13ff0..e66e0dfc2 100644 --- a/src/Core/composer.json +++ b/src/Core/composer.json @@ -34,7 +34,7 @@ }, "extra": { "branch-alias": { - "dev-master": "2.8.x-dev" + "dev-master": "2.9.x-dev" } }, "config": { diff --git a/src/Core/src/Container.php b/src/Core/src/Container.php index 7741d6725..45267022c 100644 --- a/src/Core/src/Container.php +++ b/src/Core/src/Container.php @@ -301,7 +301,7 @@ public function bind(string $alias, $resolver): void * Bind value resolver to container alias to be executed as cached. Resolver can be class name * (will be constructed only once), function array or Closure (executed only once call). * - * @param string|array|callable $resolver + * @param object|string|array|callable $resolver */ public function bindSingleton(string $alias, $resolver): void { diff --git a/src/Core/src/InjectableConfig.php b/src/Core/src/InjectableConfig.php index 71d6c5655..1e244a4f4 100644 --- a/src/Core/src/InjectableConfig.php +++ b/src/Core/src/InjectableConfig.php @@ -59,7 +59,7 @@ public function toArray(): array /** * {@inheritdoc} */ - public function offsetExists($offset) + public function offsetExists($offset): bool { return array_key_exists($offset, $this->config); } @@ -67,6 +67,7 @@ public function offsetExists($offset) /** * {@inheritdoc} */ + #[\ReturnTypeWillChange] public function offsetGet($offset) { if (!$this->offsetExists($offset)) { @@ -103,7 +104,7 @@ public function offsetUnset($offset): void /** * {@inheritdoc} */ - public function getIterator() + public function getIterator(): \Traversable { return new \ArrayIterator($this->config); } diff --git a/src/Csrf/composer.json b/src/Csrf/composer.json index 251d95862..9a5bde954 100644 --- a/src/Csrf/composer.json +++ b/src/Csrf/composer.json @@ -16,15 +16,15 @@ ], "require": { "php": ">=7.2", - "spiral/core": "^2.8", - "spiral/cookies": "^2.8", + "spiral/core": "^2.9", + "spiral/cookies": "^2.9", "psr/http-server-middleware": "^1.0" }, "require-dev": { "phpunit/phpunit": "^8.5|^9.0", "mockery/mockery": "^1.3", - "spiral/http": "^2.8", - "laminas/laminas-diactoros": "^2.3" + "spiral/http": "^2.9", + "laminas/laminas-diactoros": "^2.4" }, "autoload": { "psr-4": { @@ -38,7 +38,7 @@ }, "extra": { "branch-alias": { - "dev-master": "2.8.x-dev" + "dev-master": "2.9.x-dev" } }, "config": { diff --git a/src/DataGrid/composer.json b/src/DataGrid/composer.json index 4d9372ccc..3a6557afc 100644 --- a/src/DataGrid/composer.json +++ b/src/DataGrid/composer.json @@ -20,11 +20,12 @@ ], "require": { "php": ">=7.2", + "spiral/attributes": "^2.9", "symfony/polyfill-php73": "^1.22" }, "require-dev": { "phpunit/phpunit": "^8.5|^9.0", - "ramsey/uuid": "^3.9" + "ramsey/uuid": "^4.2" }, "autoload": { "files": [ @@ -41,7 +42,7 @@ }, "extra": { "branch-alias": { - "dev-master": "2.8.x-dev" + "dev-master": "2.9.x-dev" } }, "config": { diff --git a/src/Debug/composer.json b/src/Debug/composer.json index 285a16df6..ad348defd 100644 --- a/src/Debug/composer.json +++ b/src/Debug/composer.json @@ -16,7 +16,7 @@ ], "require": { "php": ">=7.2", - "spiral/logger": "^2.8" + "spiral/logger": "^2.9" }, "require-dev": { "phpunit/phpunit": "^8.5|^9.0" @@ -33,7 +33,7 @@ }, "extra": { "branch-alias": { - "dev-master": "2.8.x-dev" + "dev-master": "2.9.x-dev" } }, "config": { diff --git a/src/Distribution/composer.json b/src/Distribution/composer.json index 0351928b0..882aa99df 100644 --- a/src/Distribution/composer.json +++ b/src/Distribution/composer.json @@ -41,7 +41,7 @@ }, "extra": { "branch-alias": { - "dev-master": "2.8.x-dev" + "dev-master": "2.9.x-dev" } }, "scripts": { diff --git a/src/Distribution/src/Resolver/CloudFrontResolver.php b/src/Distribution/src/Resolver/CloudFrontResolver.php index 2667acaf1..33990bb5e 100644 --- a/src/Distribution/src/Resolver/CloudFrontResolver.php +++ b/src/Distribution/src/Resolver/CloudFrontResolver.php @@ -68,11 +68,7 @@ public function resolve(string $file, $expiration = null): UriInterface $date = $this->getExpirationDateTime($expiration); $url = $this->signer->getSignedUrl($this->createUrl($file), $date->getTimestamp()); - return $this->factory->createUri() - ->withScheme('https') - ->withHost($this->domain) - ->withPath($url) - ; + return $this->factory->createUri($url); } protected function assertCloudFrontAvailable(): void diff --git a/src/Distribution/tests/Resolver/StaticResolverTest.php b/src/Distribution/tests/Resolver/StaticResolverTest.php index 50e16e64a..b9f6eea36 100644 --- a/src/Distribution/tests/Resolver/StaticResolverTest.php +++ b/src/Distribution/tests/Resolver/StaticResolverTest.php @@ -16,7 +16,12 @@ public function testGuzzleResolve(): void $uri = $resolver->resolve('file.jpg'); self::assertSame('http://localhost/file.jpg', (string)$uri); - self::assertNull(error_get_last()); + + // PHP 8.1 deprecation error fix + self::assertTrue( + ($error = error_get_last()) === null || + \strpos($error['message'], 'Spiral') === false + ); } public function testGuzzleResolveWithPrefix(): void @@ -26,6 +31,11 @@ public function testGuzzleResolveWithPrefix(): void $uri = $resolver->resolve('file.jpg'); self::assertSame('http://localhost/upload/file.jpg', (string)$uri); - self::assertNull(error_get_last()); + + // PHP 8.1 deprecation error fix + self::assertTrue( + ($error = error_get_last()) === null || + \strpos($error['message'], 'Spiral') === false + ); } } diff --git a/src/Dumper/composer.json b/src/Dumper/composer.json index f2e5c6d7e..27eebd49f 100644 --- a/src/Dumper/composer.json +++ b/src/Dumper/composer.json @@ -37,7 +37,7 @@ }, "extra": { "branch-alias": { - "dev-master": "2.8.x-dev" + "dev-master": "2.9.x-dev" } }, "config": { diff --git a/src/Encrypter/composer.json b/src/Encrypter/composer.json index a61149a2b..da7035ebc 100644 --- a/src/Encrypter/composer.json +++ b/src/Encrypter/composer.json @@ -17,7 +17,7 @@ "require": { "php": ">=7.2", "ext-json": "*", - "spiral/core": "^2.8", + "spiral/core": "^2.9", "defuse/php-encryption": "^2.2" }, "require-dev": { @@ -35,7 +35,7 @@ }, "extra": { "branch-alias": { - "dev-master": "2.8.x-dev" + "dev-master": "2.9.x-dev" } }, "config": { diff --git a/src/Exceptions/composer.json b/src/Exceptions/composer.json index 732962ad8..94eb71e65 100644 --- a/src/Exceptions/composer.json +++ b/src/Exceptions/composer.json @@ -18,8 +18,8 @@ "php": ">=7.2", "ext-json": "*", "ext-mbstring": "*", - "spiral/dumper": "^2.8", - "spiral/debug": "^2.8" + "spiral/dumper": "^2.9", + "spiral/debug": "^2.9" }, "require-dev": { "phpunit/phpunit": "^8.5|^9.0" @@ -36,7 +36,7 @@ }, "extra": { "branch-alias": { - "dev-master": "2.8.x-dev" + "dev-master": "2.9.x-dev" } }, "config": { diff --git a/src/Files/composer.json b/src/Files/composer.json index 070a586e6..e0882032d 100644 --- a/src/Files/composer.json +++ b/src/Files/composer.json @@ -33,7 +33,7 @@ }, "extra": { "branch-alias": { - "dev-master": "2.8.x-dev" + "dev-master": "2.9.x-dev" } }, "config": { diff --git a/src/Filters/composer.json b/src/Filters/composer.json index 42c767d12..5167400d8 100644 --- a/src/Filters/composer.json +++ b/src/Filters/composer.json @@ -16,10 +16,10 @@ ], "require": { "php": ">=7.2", - "spiral/core": "^2.8", - "spiral/models": "^2.8", - "spiral/translator": "^2.8", - "spiral/validation": "^2.8" + "spiral/core": "^2.9", + "spiral/models": "^2.9", + "spiral/translator": "^2.9", + "spiral/validation": "^2.9" }, "require-dev": { "phpunit/phpunit": "^8.5|^9.0", @@ -37,7 +37,7 @@ }, "extra": { "branch-alias": { - "dev-master": "2.8.x-dev" + "dev-master": "2.9.x-dev" } }, "config": { diff --git a/src/Framework/Bootloader/CommandBootloader.php b/src/Framework/Bootloader/CommandBootloader.php index 6040beff6..292f644d6 100644 --- a/src/Framework/Bootloader/CommandBootloader.php +++ b/src/Framework/Bootloader/CommandBootloader.php @@ -116,8 +116,8 @@ private function configureExtensions(ConsoleBootloader $console, Container $cont */ private function configureDatabase(ConsoleBootloader $console): void { - $console->addCommand(Database\ListCommand::class); - $console->addCommand(Database\TableCommand::class); + $console->addCommand(Database\ListCommand::class, true); + $console->addCommand(Database\TableCommand::class, true); } /** @@ -126,17 +126,17 @@ private function configureDatabase(ConsoleBootloader $console): void */ private function configureCycle(ConsoleBootloader $console, ContainerInterface $container): void { - $console->addCommand(Cycle\UpdateCommand::class); + $console->addCommand(Cycle\UpdateCommand::class, true); $console->addUpdateSequence( 'cycle', '[cycle] update Cycle schema...' ); - $console->addCommand(Cycle\SyncCommand::class); + $console->addCommand(Cycle\SyncCommand::class, true); if ($container->has(Migrator::class)) { - $console->addCommand(Cycle\MigrateCommand::class); + $console->addCommand(Cycle\MigrateCommand::class, true); } } @@ -182,11 +182,11 @@ private function configureViews(ConsoleBootloader $console): void */ private function configureMigrations(ConsoleBootloader $console): void { - $console->addCommand(Migrate\InitCommand::class); - $console->addCommand(Migrate\StatusCommand::class); - $console->addCommand(Migrate\MigrateCommand::class); - $console->addCommand(Migrate\RollbackCommand::class); - $console->addCommand(Migrate\ReplayCommand::class); + $console->addCommand(Migrate\InitCommand::class, true); + $console->addCommand(Migrate\StatusCommand::class, true); + $console->addCommand(Migrate\MigrateCommand::class, true); + $console->addCommand(Migrate\RollbackCommand::class, true); + $console->addCommand(Migrate\ReplayCommand::class, true); } /** diff --git a/src/Framework/Bootloader/ConsoleBootloader.php b/src/Framework/Bootloader/ConsoleBootloader.php index 2880590fd..1487c7c9b 100644 --- a/src/Framework/Bootloader/ConsoleBootloader.php +++ b/src/Framework/Bootloader/ConsoleBootloader.php @@ -17,6 +17,7 @@ use Spiral\Command\PublishCommand; use Spiral\Config\ConfiguratorInterface; use Spiral\Config\Patch\Append; +use Spiral\Config\Patch\Prepend; use Spiral\Console\CommandLocator; use Spiral\Console\Console; use Spiral\Console\ConsoleDispatcher; @@ -72,13 +73,17 @@ public function boot(KernelInterface $kernel, ConsoleDispatcher $console): void } /** - * @param string $command + * @param class-string<\Symfony\Component\Console\Command\Command> $command + * @param bool $lowPriority A low priority command will be overwritten in a name conflict case. + * The parameter might be removed in the next major update. */ - public function addCommand(string $command): void + public function addCommand(string $command, bool $lowPriority = false): void { $this->config->modify( 'console', - new Append('commands', null, $command) + $lowPriority + ? new Prepend('commands', null, $command) + : new Append('commands', null, $command) ); } diff --git a/src/Framework/Bootloader/Http/WebsocketsBootloader.php b/src/Framework/Bootloader/Http/WebsocketsBootloader.php index 93f989d64..4f84229aa 100644 --- a/src/Framework/Bootloader/Http/WebsocketsBootloader.php +++ b/src/Framework/Bootloader/Http/WebsocketsBootloader.php @@ -25,12 +25,17 @@ */ final class WebsocketsBootloader extends Bootloader implements SingletonInterface { + /** + * @var array + */ protected const DEPENDENCIES = [ HttpBootloader::class, BroadcastBootloader::class, ]; - /** @var ConfiguratorInterface */ + /** + * @var ConfiguratorInterface + */ private $config; /** @@ -42,19 +47,16 @@ public function __construct(ConfiguratorInterface $config) } /** - * @param HttpBootloader $http + * @param HttpBootloader $http * @param EnvironmentInterface $env */ public function boot(HttpBootloader $http, EnvironmentInterface $env): void { - $this->config->setDefaults( - 'websockets', - [ - 'path' => $env->get('RR_BROADCAST_PATH', null), - 'authorizeServer' => null, - 'authorizeTopics' => [], - ] - ); + $this->config->setDefaults('websockets', [ + 'path' => $env->get('RR_BROADCAST_PATH', null), + 'authorizeServer' => null, + 'authorizeTopics' => [], + ]); if ($env->get('RR_BROADCAST_PATH', null) !== null) { $http->addMiddleware(WebsocketsMiddleware::class); @@ -70,7 +72,7 @@ public function authorizeServer(?callable $callback): void } /** - * @param string $topic + * @param string $topic * @param callable $callback */ public function authorizeTopic(string $topic, callable $callback): void diff --git a/src/Framework/Broadcast/Middleware/WebsocketsMiddleware.php b/src/Framework/Broadcast/Middleware/WebsocketsMiddleware.php index 45664242d..d3b0c5b92 100644 --- a/src/Framework/Broadcast/Middleware/WebsocketsMiddleware.php +++ b/src/Framework/Broadcast/Middleware/WebsocketsMiddleware.php @@ -27,16 +27,24 @@ */ final class WebsocketsMiddleware implements MiddlewareInterface { - /** @var WebsocketsConfig */ + /** + * @var WebsocketsConfig + */ private $config; - /** @var ScopeInterface */ + /** + * @var ScopeInterface + */ private $scope; - /** @var ResolverInterface */ + /** + * @var ResolverInterface + */ private $resolver; - /** @var ResponseFactoryInterface */ + /** + * @var ResponseFactoryInterface + */ private $responseFactory; /** @@ -81,9 +89,9 @@ public function process(ServerRequestInterface $request, RequestHandlerInterface } // topic authorization - if (is_string($request->getAttribute('ws:joinTopics', null))) { - $topics = explode(',', $request->getAttribute('ws:joinTopics')); - foreach ($topics as $topic) { + $topics = $request->getAttribute('ws:joinTopics'); + if (\is_string($topics)) { + foreach (\explode(',', $topics) as $topic) { if (!$this->authorizeTopic($request, $topic)) { return $this->responseFactory->createResponse(403); } @@ -104,6 +112,7 @@ public function process(ServerRequestInterface $request, RequestHandlerInterface private function authorizeServer(ServerRequestInterface $request): bool { $callback = $this->config->getServerCallback(); + if ($callback === null) { return true; } @@ -140,15 +149,16 @@ private function authorizeTopic(ServerRequestInterface $request, string $topic): private function invoke(ServerRequestInterface $request, callable $callback, array $parameters = []): bool { switch (true) { - case $callback instanceof \Closure || is_string($callback): + case $callback instanceof \Closure: + case \is_string($callback): $reflection = new \ReflectionFunction($callback); break; - case is_array($callback) && is_object($callback[0]): + case \is_array($callback) && \is_object($callback[0]): $reflection = (new \ReflectionObject($callback[0]))->getMethod($callback[1]); break; - case is_array($callback): + case \is_array($callback): $reflection = (new \ReflectionClass($callback[0]))->getMethod($callback[1]); break; @@ -156,16 +166,10 @@ private function invoke(ServerRequestInterface $request, callable $callback, arr throw new LogicException('Unable to invoke callable function'); } - return $this->scope->runScope( - [ - ServerRequestInterface::class => $request, - ], - function () use ($reflection, $parameters, $callback) { - return call_user_func_array( - $callback, - $this->resolver->resolveArguments($reflection, $parameters) - ); - } - ); + $scoped = function () use ($reflection, $parameters, $callback) { + return \call_user_func_array($callback, $this->resolver->resolveArguments($reflection, $parameters)); + }; + + return $this->scope->runScope([ServerRequestInterface::class => $request], $scoped); } } diff --git a/src/Framework/Console/CommandLocator.php b/src/Framework/Console/CommandLocator.php index 2b63ef409..b70cfbb0c 100644 --- a/src/Framework/Console/CommandLocator.php +++ b/src/Framework/Console/CommandLocator.php @@ -12,11 +12,14 @@ namespace Spiral\Console; use Psr\Container\ContainerInterface; +use Spiral\Console\Traits\LazyTrait; use Spiral\Tokenizer\ClassesInterface; use Symfony\Component\Console\Command\Command as SymfonyCommand; final class CommandLocator implements LocatorInterface { + use LazyTrait; + /** @var ClassesInterface */ private $classes; @@ -44,7 +47,9 @@ public function locateCommands(): array continue; } - $commands[] = $this->container->get($class->getName()); + $commands[] = $this->supportsLazyLoading($class->getName()) + ? $this->createLazyCommand($class->getName()) + : $this->container->get($class->getName()); } return $commands; diff --git a/src/Framework/Domain/Annotation/Guarded.php b/src/Framework/Domain/Annotation/Guarded.php index 9758f740c..c03afd53d 100644 --- a/src/Framework/Domain/Annotation/Guarded.php +++ b/src/Framework/Domain/Annotation/Guarded.php @@ -30,20 +30,20 @@ final class Guarded { /** - * @type string|null + * @var string|null */ public $permission; /** * @Enum({"notFound","unauthorized","forbidden","badAction","error"}) - * @type string + * @var string */ public $else = 'forbidden'; /** * Error message in case of error. * - * @type string + * @var string|null */ public $errorMessage; diff --git a/src/Framework/Domain/Annotation/Pipeline.php b/src/Framework/Domain/Annotation/Pipeline.php index 103fd1f47..78cae8575 100644 --- a/src/Framework/Domain/Annotation/Pipeline.php +++ b/src/Framework/Domain/Annotation/Pipeline.php @@ -24,12 +24,12 @@ class Pipeline /** * @var array */ - public $pipeline = []; + public $pipeline; /** * @var bool */ - public $skipNext = false; + public $skipNext; public function __construct(array $pipeline = [], bool $skipNext = false) { diff --git a/src/Framework/Domain/PipelineInterceptor.php b/src/Framework/Domain/PipelineInterceptor.php index dec4208c3..013186437 100644 --- a/src/Framework/Domain/PipelineInterceptor.php +++ b/src/Framework/Domain/PipelineInterceptor.php @@ -76,6 +76,7 @@ private function readAnnotation(string $controller, string $action): Pipeline return new Pipeline(); } + /** @var Pipeline $annotation */ $annotation = $this->reader->firstFunctionMetadata($method, Pipeline::class); return $annotation ?? new Pipeline(); } diff --git a/src/Framework/Session/SectionScope.php b/src/Framework/Session/SectionScope.php index 5249b4202..66df43243 100644 --- a/src/Framework/Session/SectionScope.php +++ b/src/Framework/Session/SectionScope.php @@ -71,7 +71,7 @@ public function __unset(string $name): void /** * @inheritDoc */ - public function getIterator() + public function getIterator(): \Traversable { return $this->getActiveSection()->getIterator(); } @@ -87,6 +87,7 @@ public function offsetExists($offset): bool /** * @inheritDoc */ + #[\ReturnTypeWillChange] public function offsetGet($offset) { return $this->getActiveSection()->offsetGet($offset); diff --git a/src/Framework/Validation/Checker/EntityChecker.php b/src/Framework/Validation/Checker/EntityChecker.php index 81e83d9f2..eb47d17e8 100644 --- a/src/Framework/Validation/Checker/EntityChecker.php +++ b/src/Framework/Validation/Checker/EntityChecker.php @@ -5,7 +5,10 @@ namespace Spiral\Validation\Checker; use Cycle\ORM\ORMInterface; +use Cycle\ORM\Select; +use Cycle\ORM\Select\Repository; use Spiral\Core\Container\SingletonInterface; +use Spiral\Database\Injection\Expression; use Spiral\Validation\AbstractChecker; class EntityChecker extends AbstractChecker implements SingletonInterface @@ -33,15 +36,22 @@ public function __construct(ORMInterface $orm) * @param string|int $value * @param string $role * @param string|null $field + * @param bool $ignoreCase * @return bool */ - public function exists($value, string $role, ?string $field = null): bool + public function exists($value, string $role, ?string $field = null, bool $ignoreCase = false): bool { $repository = $this->orm->getRepository($role); if ($field === null) { return $repository->findByPK($value) !== null; } + if ($ignoreCase && $repository instanceof Repository) { + return $this + ->addCaseInsensitiveWhere($repository->select(), $field, $value) + ->fetchOne() !== null; + } + return $repository->findOne([$field => $value]) !== null; } @@ -50,9 +60,10 @@ public function exists($value, string $role, ?string $field = null): bool * @param string $role * @param string $field * @param string[] $withFields + * @param bool $ignoreCase * @return bool */ - public function unique($value, string $role, string $field, array $withFields = []): bool + public function unique($value, string $role, string $field, array $withFields = [], bool $ignoreCase = false): bool { $values = $this->withValues($withFields); $values[$field] = $value; @@ -61,7 +72,19 @@ public function unique($value, string $role, string $field, array $withFields = return true; } - return $this->orm->getRepository($role)->findOne($values) === null; + $repository = $this->orm->getRepository($role); + + if ($ignoreCase && $repository instanceof Repository) { + $select = $repository->select(); + + foreach ($values as $field => $fieldValue) { + $this->addCaseInsensitiveWhere($select, $field, $fieldValue); + } + + return $select->fetchOne() === null; + } + + return $repository->findOne($values) === null; } /** @@ -101,4 +124,26 @@ private function isProvidedByContext(string $role, array $values): bool return true; } + + /** + * @param Select $select + * @param string $field + * @param mixed $value + * @return Select + */ + private function addCaseInsensitiveWhere(Select $select, string $field, $value): Select + { + if (!is_string($value)) { + return $select->where($field, $value); + } + + $queryBuilder = $select->getBuilder(); + + return $select + ->where( + new Expression("LOWER({$queryBuilder->resolve($field)})"), + mb_strtolower($value) + ) + ; + } } diff --git a/src/Hmvc/composer.json b/src/Hmvc/composer.json index fbd5efd99..0eb917f85 100644 --- a/src/Hmvc/composer.json +++ b/src/Hmvc/composer.json @@ -16,7 +16,7 @@ ], "require": { "php": ">=7.2", - "spiral/core": "^2.8" + "spiral/core": "^2.9" }, "require-dev": { "phpunit/phpunit": "^8.5|^9.0" @@ -33,7 +33,7 @@ }, "extra": { "branch-alias": { - "dev-master": "2.8.x-dev" + "dev-master": "2.9.x-dev" } }, "config": { diff --git a/src/Http/composer.json b/src/Http/composer.json index 33a4463c3..7e2bb93ee 100644 --- a/src/Http/composer.json +++ b/src/Http/composer.json @@ -18,9 +18,9 @@ "php": ">=7.2", "ext-json": "*", "ext-mbstring": "*", - "spiral/core": "^2.8", - "spiral/files": "^2.8", - "spiral/streams": "^2.8", + "spiral/core": "^2.9", + "spiral/files": "^2.9", + "spiral/streams": "^2.9", "psr/http-message": "^1.0", "psr/http-factory": "^1.0", "psr/http-server-middleware": "^1.0", @@ -29,7 +29,7 @@ "require-dev": { "phpunit/phpunit": "^8.5|^9.0", "mockery/mockery": "^1.3", - "laminas/laminas-diactoros": "^2.3" + "laminas/laminas-diactoros": "^2.4" }, "autoload": { "psr-4": { @@ -43,7 +43,7 @@ }, "extra": { "branch-alias": { - "dev-master": "2.8.x-dev" + "dev-master": "2.9.x-dev" } }, "config": { diff --git a/src/Http/src/Request/InputBag.php b/src/Http/src/Request/InputBag.php index 15a8db23c..a1a627eae 100644 --- a/src/Http/src/Request/InputBag.php +++ b/src/Http/src/Request/InputBag.php @@ -87,7 +87,7 @@ public function fetch(array $keys, bool $fill = false, $filler = null) /** * {@inheritdoc} */ - public function offsetExists($offset) + public function offsetExists($offset): bool { return $this->has($offset); } @@ -109,6 +109,7 @@ public function has(string $name): bool /** * {@inheritdoc} */ + #[\ReturnTypeWillChange] public function offsetGet($offset) { return $this->get($offset); diff --git a/src/Http/tests/Json.php b/src/Http/tests/Json.php index 9f524ee3f..7ba1ce2c8 100644 --- a/src/Http/tests/Json.php +++ b/src/Http/tests/Json.php @@ -20,6 +20,7 @@ public function __construct($data) $this->data = $data; } + #[\ReturnTypeWillChange] public function jsonSerialize() { return $this->data; diff --git a/src/Logger/composer.json b/src/Logger/composer.json index 94839fe2f..f75576aed 100644 --- a/src/Logger/composer.json +++ b/src/Logger/composer.json @@ -17,7 +17,7 @@ "require": { "php": ">=7.2", "psr/log": "^1.0", - "spiral/core": "^2.8" + "spiral/core": "^2.9" }, "require-dev": { "phpunit/phpunit": "^8.5|^9.0", @@ -35,7 +35,7 @@ }, "extra": { "branch-alias": { - "dev-master": "2.8.x-dev" + "dev-master": "2.9.x-dev" } }, "config": { diff --git a/src/Mailer/composer.json b/src/Mailer/composer.json index 16d79424f..6f387e140 100644 --- a/src/Mailer/composer.json +++ b/src/Mailer/composer.json @@ -33,7 +33,7 @@ }, "extra": { "branch-alias": { - "dev-master": "2.8.x-dev" + "dev-master": "2.9.x-dev" } }, "config": { diff --git a/src/Models/composer.json b/src/Models/composer.json index cc46d4b1e..0d9d2a39a 100644 --- a/src/Models/composer.json +++ b/src/Models/composer.json @@ -33,7 +33,7 @@ }, "extra": { "branch-alias": { - "dev-master": "2.8.x-dev" + "dev-master": "2.9.x-dev" } }, "config": { diff --git a/src/Models/src/AbstractEntity.php b/src/Models/src/AbstractEntity.php index a56110084..ef90cde54 100644 --- a/src/Models/src/AbstractEntity.php +++ b/src/Models/src/AbstractEntity.php @@ -199,7 +199,7 @@ public function getFields(bool $filter = true): array /** * {@inheritdoc} */ - public function offsetExists($offset) + public function offsetExists($offset): bool { return $this->__isset($offset); } @@ -207,6 +207,7 @@ public function offsetExists($offset) /** * {@inheritdoc} */ + #[\ReturnTypeWillChange] public function offsetGet($offset) { return $this->getField($offset); diff --git a/src/Pagination/composer.json b/src/Pagination/composer.json index be70519c8..0b696cab6 100644 --- a/src/Pagination/composer.json +++ b/src/Pagination/composer.json @@ -33,7 +33,7 @@ }, "extra": { "branch-alias": { - "dev-master": "2.8.x-dev" + "dev-master": "2.9.x-dev" } }, "config": { diff --git a/src/Pagination/src/Paginator.php b/src/Pagination/src/Paginator.php index c8bca4a2e..927f2be91 100644 --- a/src/Pagination/src/Paginator.php +++ b/src/Pagination/src/Paginator.php @@ -130,10 +130,7 @@ public function paginate(PaginableInterface $target): PaginatorInterface return $paginator; } - /** - * @return int - */ - public function count() + public function count(): int { return $this->count; } diff --git a/src/Prototype/composer.json b/src/Prototype/composer.json index 038478734..d3c001d9b 100644 --- a/src/Prototype/composer.json +++ b/src/Prototype/composer.json @@ -19,8 +19,8 @@ "ext-json": "*", "nikic/php-parser": "^4.1", "doctrine/inflector": "^1.4|^2.0", - "spiral/console": "^2.8", - "spiral/annotations": "^2.8" + "spiral/console": "^2.9", + "spiral/annotations": "^2.9" }, "autoload": { "psr-4": { @@ -31,7 +31,7 @@ "phpunit/phpunit": "^8.5|^9.0", "mockery/mockery": "^1.3", "cycle/orm": "^1.2.6", - "spiral/debug": "^2.8" + "spiral/debug": "^2.9" }, "autoload-dev": { "psr-4": { @@ -40,7 +40,7 @@ }, "extra": { "branch-alias": { - "dev-master": "2.8.x-dev" + "dev-master": "2.9.x-dev" } }, "config": { diff --git a/src/Reactor/composer.json b/src/Reactor/composer.json index 14c5f1ddc..3e51ad1ef 100644 --- a/src/Reactor/composer.json +++ b/src/Reactor/composer.json @@ -35,7 +35,7 @@ }, "extra": { "branch-alias": { - "dev-master": "2.8.x-dev" + "dev-master": "2.9.x-dev" } }, "config": { diff --git a/src/Reactor/src/Aggregator.php b/src/Reactor/src/Aggregator.php index 52aec1efd..143c2a061 100644 --- a/src/Reactor/src/Aggregator.php +++ b/src/Reactor/src/Aggregator.php @@ -140,7 +140,7 @@ public function getIterator(): ArrayIterator /** * {@inheritdoc} */ - public function offsetExists($offset) + public function offsetExists($offset): bool { return $this->has($offset); } @@ -148,6 +148,7 @@ public function offsetExists($offset) /** * {@inheritdoc} */ + #[\ReturnTypeWillChange] public function offsetGet($offset) { return $this->get($offset); diff --git a/src/Router/composer.json b/src/Router/composer.json index 6d195c62d..bb72de52c 100644 --- a/src/Router/composer.json +++ b/src/Router/composer.json @@ -17,16 +17,16 @@ "require": { "php": ">=7.2", "ext-json": "*", - "spiral/core": "^2.8", - "spiral/hmvc": "^2.8", - "spiral/http": "^2.8", + "spiral/core": "^2.9", + "spiral/hmvc": "^2.9", + "spiral/http": "^2.9", "cocur/slugify": "^3.2", "doctrine/inflector": "^1.4|^2.0" }, "require-dev": { "phpunit/phpunit": "^8.5|^9.0", "mockery/mockery": "^1.3", - "laminas/laminas-diactoros": "^2.3" + "laminas/laminas-diactoros": "^2.4" }, "autoload": { "psr-4": { @@ -40,7 +40,7 @@ }, "extra": { "branch-alias": { - "dev-master": "2.8.x-dev" + "dev-master": "2.9.x-dev" } }, "config": { diff --git a/src/Scaffolder/composer.json b/src/Scaffolder/composer.json index 2a89f687e..2cda03143 100644 --- a/src/Scaffolder/composer.json +++ b/src/Scaffolder/composer.json @@ -21,7 +21,7 @@ ], "require": { "php": ">=7.2", - "spiral/reactor": "^2.8", + "spiral/reactor": "^2.9", "cocur/slugify": "^3.2", "doctrine/inflector": "^1.4|^2.0" }, @@ -29,13 +29,13 @@ "phpunit/phpunit": "^8.5|^9.0", "cycle/orm": "^1.2.6", "cycle/annotated": "^2.0.6", - "spiral/boot": "^2.8", - "spiral/console": "^2.8", - "spiral/core": "^2.8", - "spiral/filters": "^2.8", - "spiral/http": "^2.8", - "spiral/migrations": "^2.1", - "spiral/prototype": "^2.8", + "spiral/boot": "^2.9", + "spiral/console": "^2.9", + "spiral/core": "^2.9", + "spiral/filters": "^2.9", + "spiral/http": "^2.9", + "spiral/migrations": "^2.2", + "spiral/prototype": "^2.9", "spiral/jobs": "^2.2" }, "autoload": { @@ -53,7 +53,7 @@ }, "extra": { "branch-alias": { - "dev-master": "2.8.x-dev" + "dev-master": "2.9.x-dev" } }, "config": { diff --git a/src/Security/composer.json b/src/Security/composer.json index 58754756e..2234d3297 100644 --- a/src/Security/composer.json +++ b/src/Security/composer.json @@ -16,7 +16,7 @@ ], "require": { "php": ">=7.2", - "spiral/core": "^2.8" + "spiral/core": "^2.9" }, "require-dev": { "phpunit/phpunit": "^8.5|^9.0" @@ -33,7 +33,7 @@ }, "extra": { "branch-alias": { - "dev-master": "2.8.x-dev" + "dev-master": "2.9.x-dev" } }, "config": { diff --git a/src/SendIt/composer.json b/src/SendIt/composer.json index 7622c6558..e46d59239 100644 --- a/src/SendIt/composer.json +++ b/src/SendIt/composer.json @@ -17,16 +17,16 @@ "require": { "php": ">=7.2", "ext-json": "*", - "spiral/logger": "^2.8", - "spiral/mailer": "^2.8", + "spiral/jobs": "^2.2", + "spiral/logger": "^2.9", + "spiral/mailer": "^2.9", "symfony/mailer": "^5.1" }, "require-dev": { "phpunit/phpunit": "^8.5|^9.0", "mockery/mockery": "^1.3", - "spiral/jobs": "^2.2", - "spiral/views": "^2.8", - "spiral/stempler-bridge": "^2.8" + "spiral/views": "^2.9", + "spiral/stempler-bridge": "^2.9" }, "autoload": { "psr-4": { @@ -40,7 +40,7 @@ }, "extra": { "branch-alias": { - "dev-master": "2.8.x-dev" + "dev-master": "2.9.x-dev" } }, "config": { diff --git a/src/Session/composer.json b/src/Session/composer.json index e469df62a..4ab172e54 100644 --- a/src/Session/composer.json +++ b/src/Session/composer.json @@ -16,8 +16,8 @@ ], "require": { "php": ">=7.2", - "spiral/core": "^2.8", - "spiral/files": "^2.8" + "spiral/core": "^2.9", + "spiral/files": "^2.9" }, "require-dev": { "phpunit/phpunit": "^8.5|^9.0" @@ -34,7 +34,7 @@ }, "extra": { "branch-alias": { - "dev-master": "2.8.x-dev" + "dev-master": "2.9.x-dev" } }, "config": { diff --git a/src/Session/src/Handler/FileHandler.php b/src/Session/src/Handler/FileHandler.php index 3f3113b20..f3052eefd 100644 --- a/src/Session/src/Handler/FileHandler.php +++ b/src/Session/src/Handler/FileHandler.php @@ -58,13 +58,16 @@ public function destroy($session_id): bool * @inheritdoc * @codeCoverageIgnore */ - public function gc($maxlifetime): void + #[\ReturnTypeWillChange] + public function gc($maxlifetime) { foreach ($this->files->getFiles($this->directory) as $filename) { if ($this->files->time($filename) < time() - $maxlifetime) { $this->files->delete($filename); } } + + return $maxlifetime; } /** diff --git a/src/Session/src/Handler/NullHandler.php b/src/Session/src/Handler/NullHandler.php index bfda58edf..601a0a095 100644 --- a/src/Session/src/Handler/NullHandler.php +++ b/src/Session/src/Handler/NullHandler.php @@ -35,9 +35,10 @@ public function destroy($session_id): bool /** * @inheritdoc */ - public function gc($maxlifetime): bool + #[\ReturnTypeWillChange] + public function gc($maxlifetime) { - return true; + return $maxlifetime; } /** diff --git a/src/Session/src/SessionSection.php b/src/Session/src/SessionSection.php index f3bed463b..640096942 100644 --- a/src/Session/src/SessionSection.php +++ b/src/Session/src/SessionSection.php @@ -79,7 +79,7 @@ public function getName(): string /** * @inheritdoc */ - public function getIterator() + public function getIterator(): \Traversable { return new \ArrayIterator($this->getAll()); } @@ -166,6 +166,7 @@ public function offsetExists($offset): bool /** * @inheritdoc */ + #[\ReturnTypeWillChange] public function offsetGet($offset) { return $this->get($offset); diff --git a/src/Session/tests/NullHandlerTest.php b/src/Session/tests/NullHandlerTest.php index 37a71ba60..b12a3c4d7 100644 --- a/src/Session/tests/NullHandlerTest.php +++ b/src/Session/tests/NullHandlerTest.php @@ -21,7 +21,7 @@ public function testNullHandler(): void $handler = new NullHandler(); $this->assertTrue($handler->destroy('abc')); - $this->assertTrue($handler->gc(1)); + $this->assertSame(1, $handler->gc(1)); $this->assertTrue($handler->open('path', 1)); $this->assertSame('', $handler->read('')); $this->assertTrue($handler->write('abc', 'data')); diff --git a/src/Snapshots/composer.json b/src/Snapshots/composer.json index 0b167c9d5..e00510163 100644 --- a/src/Snapshots/composer.json +++ b/src/Snapshots/composer.json @@ -32,7 +32,7 @@ }, "extra": { "branch-alias": { - "dev-master": "2.8.x-dev" + "dev-master": "2.9.x-dev" } }, "config": { diff --git a/src/Stempler/composer.json b/src/Stempler/composer.json index c96bb401a..e9e1823a4 100644 --- a/src/Stempler/composer.json +++ b/src/Stempler/composer.json @@ -22,7 +22,7 @@ "require-dev": { "phpunit/phpunit": "^8.5|^9.0", "mockery/mockery": "^1.3", - "spiral/dumper": "^2.8" + "spiral/dumper": "^2.9" }, "autoload": { "files": [ @@ -39,7 +39,7 @@ }, "extra": { "branch-alias": { - "dev-master": "2.8.x-dev" + "dev-master": "2.9.x-dev" } }, "config": { diff --git a/src/Stempler/src/Compiler/SourceMap.php b/src/Stempler/src/Compiler/SourceMap.php index 4fce690be..c0c97eb33 100644 --- a/src/Stempler/src/Compiler/SourceMap.php +++ b/src/Stempler/src/Compiler/SourceMap.php @@ -17,7 +17,7 @@ /** * Stores and resolves offsets and line numbers between templates. */ -final class SourceMap implements \Serializable +final class SourceMap { /** @var array */ private $paths = []; @@ -28,6 +28,20 @@ final class SourceMap implements \Serializable /** @var Source[] */ private $sourceCache; + public function __serialize(): array + { + return [ + 'paths' => $this->paths, + 'lines' => $this->lines, + ]; + } + + public function __unserialize(array $data): void + { + $this->paths = $data['paths']; + $this->lines = $data['lines']; + } + /** * Get all template paths involved in final template. */ @@ -74,10 +88,7 @@ public function getStack(int $line): array */ public function serialize() { - return json_encode([ - 'paths' => $this->paths, - 'lines' => $this->lines, - ]); + return json_encode($this->__serialize()); } /** @@ -85,10 +96,7 @@ public function serialize() */ public function unserialize($serialized): void { - $data = json_decode($serialized, true); - - $this->paths = $data['paths']; - $this->lines = $data['lines']; + $this->__unserialize(json_decode($serialized, true)); } public static function calculate(string $content, array $locations, LoaderInterface $loader): SourceMap diff --git a/src/Stempler/src/Lexer/Buffer.php b/src/Stempler/src/Lexer/Buffer.php index cb1b31ca1..3fad21bc4 100644 --- a/src/Stempler/src/Lexer/Buffer.php +++ b/src/Stempler/src/Lexer/Buffer.php @@ -40,7 +40,7 @@ public function __construct(\Generator $generator, int $offset = 0) * * @return \Generator */ - public function getIterator() + public function getIterator(): \Traversable { while ($n = $this->next()) { yield $n; diff --git a/src/Stempler/src/Lexer/Grammar/Dynamic/DirectiveGrammar.php b/src/Stempler/src/Lexer/Grammar/Dynamic/DirectiveGrammar.php index 6b21c5f32..e56f8c3aa 100644 --- a/src/Stempler/src/Lexer/Grammar/Dynamic/DirectiveGrammar.php +++ b/src/Stempler/src/Lexer/Grammar/Dynamic/DirectiveGrammar.php @@ -98,7 +98,7 @@ public function parse(Buffer $src, int $offset): bool * * @return \Generator|\Traversable */ - public function getIterator() + public function getIterator(): \Traversable { if ($this->tokens === []) { throw new \LogicException('Directive not parsed'); diff --git a/src/Storage/README.md b/src/Storage/README.md index d51060fa5..a9f50cdda 100644 --- a/src/Storage/README.md +++ b/src/Storage/README.md @@ -1,3 +1,4 @@ # Storage Component [![Latest Stable Version](https://poser.pugx.org/spiral/storage/version)](https://packagist.org/packages/spiral/storage) -[![Build Status](https://github.com/spiral/storage/workflows/build/badge.svg](https://github.com/spiral/storage/actions) +[![Build Status](https://github.com/spiral/storage/workflows/build/badge.svg)](https://github.com/spiral/storage/actions) +[![Codecov](https://codecov.io/gh/spiral/storage/graph/badge.svg)](https://codecov.io/gh/spiral/storage) diff --git a/src/Storage/composer.json b/src/Storage/composer.json index 0ded9220f..07336488f 100644 --- a/src/Storage/composer.json +++ b/src/Storage/composer.json @@ -20,7 +20,7 @@ ], "require": { "php": ">=7.2", - "spiral/distribution": "^2.8", + "spiral/distribution": "^2.9", "symfony/polyfill-php80": "^1.22", "league/flysystem": "^2.0" }, diff --git a/src/Streams/composer.json b/src/Streams/composer.json index 3853f20e2..321885af0 100644 --- a/src/Streams/composer.json +++ b/src/Streams/composer.json @@ -21,8 +21,8 @@ "require-dev": { "phpunit/phpunit": "^8.5|^9.0", "mockery/mockery": "^1.3", - "spiral/files": "^2.8", - "laminas/laminas-diactoros": "^2.3" + "spiral/files": "^2.9", + "laminas/laminas-diactoros": "^2.4" }, "autoload": { "psr-4": { @@ -36,7 +36,7 @@ }, "extra": { "branch-alias": { - "dev-master": "2.8.x-dev" + "dev-master": "2.9.x-dev" } }, "config": { diff --git a/src/Tokenizer/composer.json b/src/Tokenizer/composer.json index 0866cbb1a..287d21fec 100644 --- a/src/Tokenizer/composer.json +++ b/src/Tokenizer/composer.json @@ -16,8 +16,8 @@ ], "require": { "php": ">=7.2", - "spiral/core": "^2.8", - "spiral/logger": "^2.8", + "spiral/core": "^2.9", + "spiral/logger": "^2.9", "symfony/finder": "^5.1" }, "require-dev": { @@ -35,7 +35,7 @@ }, "extra": { "branch-alias": { - "dev-master": "2.8.x-dev" + "dev-master": "2.9.x-dev" } }, "config": { diff --git a/src/Translator/composer.json b/src/Translator/composer.json index ac1641f52..6cf089a5d 100644 --- a/src/Translator/composer.json +++ b/src/Translator/composer.json @@ -16,9 +16,9 @@ ], "require": { "php": ">=7.2", - "spiral/core": "^2.8", - "spiral/logger": "^2.8", - "spiral/tokenizer": "^2.8", + "spiral/core": "^2.9", + "spiral/logger": "^2.9", + "spiral/tokenizer": "^2.9", "symfony/translation": "^5.1" }, "require-dev": { @@ -40,7 +40,7 @@ }, "extra": { "branch-alias": { - "dev-master": "2.8.x-dev" + "dev-master": "2.9.x-dev" } }, "config": { diff --git a/src/Validation/composer.json b/src/Validation/composer.json index 117d18288..9932c0794 100644 --- a/src/Validation/composer.json +++ b/src/Validation/composer.json @@ -17,15 +17,15 @@ "require": { "php": ">=7.2", "ext-json": "*", - "spiral/core": "^2.8", - "spiral/files": "^2.8", - "spiral/streams": "^2.8", - "spiral/translator": "^2.8" + "spiral/core": "^2.9", + "spiral/files": "^2.9", + "spiral/streams": "^2.9", + "spiral/translator": "^2.9" }, "require-dev": { "phpunit/phpunit": "^8.5|^9.0", "mockery/mockery": "^1.3", - "laminas/laminas-diactoros": "^2.3" + "laminas/laminas-diactoros": "^2.4" }, "autoload": { "psr-4": { @@ -39,7 +39,7 @@ }, "extra": { "branch-alias": { - "dev-master": "2.8.x-dev" + "dev-master": "2.9.x-dev" } }, "config": { diff --git a/src/Validation/src/Checker/ArrayChecker.php b/src/Validation/src/Checker/ArrayChecker.php index 21f3630c9..c9fd10348 100644 --- a/src/Validation/src/Checker/ArrayChecker.php +++ b/src/Validation/src/Checker/ArrayChecker.php @@ -9,6 +9,16 @@ class ArrayChecker extends AbstractChecker { + /** + * {@inheritdoc} + */ + public const MESSAGES = [ + 'count' => '[[Number of elements must be exactly {1}.]]', + 'longer' => '[[Number of elements must be equal to or greater than {1}.]]', + 'shorter' => '[[Number of elements must be equal to or less than {1}.]]', + 'range' => '[[Number of elements must be between {1} and {2}.]]', + ]; + /** @var ValidationInterface */ private $validation; @@ -31,4 +41,42 @@ public function of($value, $checker): bool return true; } + + public function count($value, int $length): bool + { + if (!is_array($value) && !$value instanceof \Countable) { + return false; + } + + return count($value) === $length; + } + + public function shorter($value, int $length): bool + { + if (!is_array($value) && !$value instanceof \Countable) { + return false; + } + + return count($value) <= $length; + } + + public function longer($value, int $length): bool + { + if (!is_array($value) && !$value instanceof \Countable) { + return false; + } + + return count($value) >= $length; + } + + public function range($value, int $min, int $max): bool + { + if (!is_array($value) && !$value instanceof \Countable) { + return false; + } + + $count = \count($value); + + return $count >= $min && $count <= $max; + } } diff --git a/src/Validation/src/Checker/NumberChecker.php b/src/Validation/src/Checker/NumberChecker.php index 66ac51eb8..475120b19 100644 --- a/src/Validation/src/Checker/NumberChecker.php +++ b/src/Validation/src/Checker/NumberChecker.php @@ -24,8 +24,8 @@ final class NumberChecker extends AbstractChecker implements SingletonInterface */ public const MESSAGES = [ 'range' => '[[Your value should be in range of {1}-{2}.]]', - 'higher' => '[[Your value should be higher than {1}.]]', - 'lower' => '[[Your value should be lower than {1}.]]', + 'higher' => '[[Your value should be equal to or higher than {1}.]]', + 'lower' => '[[Your value should be equal to or lower than {1}.]]', ]; /** diff --git a/src/Validation/src/CheckerRule.php b/src/Validation/src/CheckerRule.php index 8bd54b6c1..95fd0e72a 100644 --- a/src/Validation/src/CheckerRule.php +++ b/src/Validation/src/CheckerRule.php @@ -11,10 +11,13 @@ namespace Spiral\Validation; +use Spiral\Translator\Traits\TranslatorTrait; use Spiral\Translator\Translator; final class CheckerRule extends AbstractRule { + use TranslatorTrait; + /** @var CheckerInterface */ private $checker; @@ -61,7 +64,7 @@ public function validate(ValidatorInterface $v, string $field, $value): bool public function getMessage(string $field, $value): string { if (!empty($this->message)) { - return Translator::interpolate( + return $this->say( $this->message, array_merge([$value, $field], $this->args) ); diff --git a/src/Validation/tests/Checkers/ArrayTest.php b/src/Validation/tests/Checkers/ArrayTest.php index d4fbdeb90..acab2b5a5 100644 --- a/src/Validation/tests/Checkers/ArrayTest.php +++ b/src/Validation/tests/Checkers/ArrayTest.php @@ -28,4 +28,69 @@ public function testOf(): void $this->assertFalse($checker->of(1, 'is_int')); $this->assertFalse($checker->of([1], 'is_string')); } + + public function testCount(): void + { + /** @var ArrayChecker $checker */ + $checker = $this->container->get(ArrayChecker::class); + + $this->assertFalse($checker->count('foobar', 1)); + $this->assertTrue($checker->count($this->createCountable(2), 2)); + $this->assertTrue($checker->count([1, 2], 2)); + $this->assertFalse($checker->count([1, 2], 3)); + } + + public function testLonger(): void + { + /** @var ArrayChecker $checker */ + $checker = $this->container->get(ArrayChecker::class); + + $this->assertFalse($checker->longer('foobar', 1)); + $this->assertTrue($checker->longer($this->createCountable(2), 1)); + $this->assertTrue($checker->longer([1, 2], 1)); + $this->assertTrue($checker->longer([1, 2], 2)); + $this->assertFalse($checker->longer([1, 2], 3)); + } + + public function testShorter(): void + { + /** @var ArrayChecker $checker */ + $checker = $this->container->get(ArrayChecker::class); + + $this->assertFalse($checker->shorter('foobar', 1)); + $this->assertTrue($checker->shorter($this->createCountable(2), 3)); + $this->assertTrue($checker->shorter([1, 2], 3)); + $this->assertTrue($checker->shorter([1, 2], 2)); + $this->assertFalse($checker->shorter([1, 2], 1)); + } + + public function testRange(): void + { + /** @var ArrayChecker $checker */ + $checker = $this->container->get(ArrayChecker::class); + + $this->assertFalse($checker->range('foobar', 1, 2)); + $this->assertTrue($checker->range($this->createCountable(2), 0, 2)); + $this->assertTrue($checker->range([1, 2], 1, 2)); + $this->assertTrue($checker->range([1, 2], 2, 3)); + $this->assertFalse($checker->range([1, 2], 0, 0)); + $this->assertFalse($checker->range([1, 2], 3, 4)); + } + + private function createCountable(int $count): \Countable + { + return new class($count) implements \Countable { + private $count; + + public function __construct(int $count) + { + $this->count = $count; + } + + public function count(): int + { + return $this->count; + } + }; + } } diff --git a/src/Views/composer.json b/src/Views/composer.json index b7bcedd11..96c2c2b6d 100644 --- a/src/Views/composer.json +++ b/src/Views/composer.json @@ -16,8 +16,8 @@ ], "require": { "php": ">=7.2", - "spiral/core": "^2.8", - "spiral/files": "^2.8" + "spiral/core": "^2.9", + "spiral/files": "^2.9" }, "require-dev": { "phpunit/phpunit": "^8.5|^9.0", @@ -35,7 +35,7 @@ }, "extra": { "branch-alias": { - "dev-master": "2.8.x-dev" + "dev-master": "2.9.x-dev" } }, "config": { diff --git a/tests/Framework/Interceptor/GuardedTest.php b/tests/Framework/Interceptor/GuardedTest.php index 30a33f0f5..20420125f 100644 --- a/tests/Framework/Interceptor/GuardedTest.php +++ b/tests/Framework/Interceptor/GuardedTest.php @@ -32,6 +32,15 @@ public function testInvalidAnnotationConfiguration(): void $core->callAction(DemoController::class, 'guardedButNoName', []); } + public function testInvalidAnnotationConfigurationWithAttribute(): void + { + /** @var CoreInterface $core */ + $core = $this->app->get(CoreInterface::class); + + $this->expectException(InterceptorException::class); + $core->callAction(DemoController::class, 'guardedButNoNameAttribute', []); + } + public function testInvalidAnnotationConfigurationIfEmptyGuarded(): void { /** @var CoreInterface $core */ @@ -107,6 +116,16 @@ public function testAllowed(): void $this->assertSame('ok', $core->callAction(DemoController::class, 'do', [])); } + public function testAllowedWithAttribute(): void + { + /** @var CoreInterface $core */ + $core = $this->app->get(CoreInterface::class); + + $this->app->getContainer()->bind(ActorInterface::class, new Actor(['user'])); + + $this->assertSame('ok', $core->callAction(DemoController::class, 'doAttribute', [])); + } + public function testNotAllowed3(): void { /** @var CoreInterface $core */ @@ -126,4 +145,13 @@ public function testAllowed2(): void $this->app->getContainer()->bind(ActorInterface::class, new Actor(['demo'])); $this->assertSame('ok', $core->callAction(Demo2Controller::class, 'do1', [])); } + + public function testNotAllowed2WithAttribute(): void + { + /** @var CoreInterface $core */ + $core = $this->app->get(CoreInterface::class); + + $this->app->getContainer()->bind(ActorInterface::class, new Actor(['demo'])); + $this->assertSame('ok', $core->callAction(Demo2Controller::class, 'do1Attribute', [])); + } } diff --git a/tests/Framework/Interceptor/PipelineInterceptorTest.php b/tests/Framework/Interceptor/PipelineInterceptorTest.php index fcde9eca1..6eeb83dcc 100644 --- a/tests/Framework/Interceptor/PipelineInterceptorTest.php +++ b/tests/Framework/Interceptor/PipelineInterceptorTest.php @@ -54,4 +54,43 @@ public function testSkipIfFirst(): void //interceptors after current pipeline are ignored $this->assertSame(['first', 'three', 'two', 'one'], $output); } + + public function testWithAttribute(): void + { + $response = $this->get('/intercepted/withAttribute')->getBody(); + $output = json_decode((string)$response, true); + $this->assertSame(['withAttribute', 'three', 'two', 'one'], $output); + } + + public function testMixAttribute(): void + { + $response = $this->get('/intercepted/mixAttribute')->getBody(); + $output = json_decode((string)$response, true); + //pipeline interceptors are injected into the middle + $this->assertSame(['mixAttribute', 'six', 'three', 'two', 'one', 'five', 'four'], $output); + } + + public function testDupAttribute(): void + { + $response = $this->get('/intercepted/dupAttribute')->getBody(); + $output = json_decode((string)$response, true); + //pipeline interceptors are added to the end + $this->assertSame(['dupAttribute', 'three', 'two', 'one', 'three', 'two', 'one'], $output); + } + + public function testSkipNextAttribute(): void + { + $response = $this->get('/intercepted/skipAttribute')->getBody(); + $output = json_decode((string)$response, true); + //interceptors after current pipeline are ignored + $this->assertSame(['skipAttribute', 'three', 'two', 'one', 'one'], $output); + } + + public function testSkipIfFirstAttribute(): void + { + $response = $this->get('/intercepted/firstAttribute')->getBody(); + $output = json_decode((string)$response, true); + //interceptors after current pipeline are ignored + $this->assertSame(['firstAttribute', 'three', 'two', 'one'], $output); + } } diff --git a/tests/Framework/Validation/EntityCheckerTest.php b/tests/Framework/Validation/EntityCheckerTest.php index aeeaeefa1..b5b167d6d 100644 --- a/tests/Framework/Validation/EntityCheckerTest.php +++ b/tests/Framework/Validation/EntityCheckerTest.php @@ -45,6 +45,16 @@ public function testExistsByPK(): void $this->assertTrue($this->exists(1)); } + public function testCaseInsensitiveExists(): void + { + $transaction = $this->app->get(TransactionInterface::class); + $transaction->persist(new User('Valentin')); + $transaction->run(); + + $this->assertTrue($this->exists('vALenTIn', 'name', true)); + $this->assertFalse($this->exists('valentin', 'name', false)); + } + /** * @throws Throwable */ @@ -74,6 +84,18 @@ public function testSimpleUnique(): void $this->assertFalse($this->isUnique('Valentin', 'name')); } + public function testCaseInsensitiveUnique(): void + { + $transaction = $this->app->get(TransactionInterface::class); + $transaction->persist(new User('Valentin')); + $transaction->run(); + + $this->assertFalse($this->isUnique('vaLeNtIN', 'name', [], null, [], true)); + $this->assertFalse($this->isUnique('1', 'id', ['name' => 'valEntIn'], null, ['name'], true)); + $this->assertTrue($this->isUnique('vaLeNtIN', 'name')); + $this->assertTrue($this->isUnique('1', 'id', ['name' => 'valEntIn'], null, ['name'])); + } + /** * @throws Throwable */ @@ -107,15 +129,16 @@ public function testContextualUnique(): void /** * @param mixed $value * @param string|null $field + * @param bool $ignoreCase * @return bool */ - private function exists($value, ?string $field = null): bool + private function exists($value, ?string $field = null, bool $ignoreCase = false): bool { /** @var ValidationInterface $validator */ $validator = $this->app->get(ValidationInterface::class); $validator = $validator->validate( ['value' => $value], - ['value' => [['entity::exists', User::class, $field]]] + ['value' => [['entity::exists', User::class, $field, $ignoreCase]]] ); return $validator->isValid(); @@ -127,6 +150,7 @@ private function exists($value, ?string $field = null): bool * @param array $data * @param object|null $context * @param string[] $fields + * @param bool $ignoreCase * @return bool */ private function isUnique( @@ -134,13 +158,14 @@ private function isUnique( string $field, array $data = [], ?object $context = null, - array $fields = [] + array $fields = [], + bool $ignoreCase = false ): bool { /** @var ValidationInterface $validator */ $validator = $this->app->get(ValidationInterface::class); $validator = $validator->validate( ['value' => $value] + $data, - ['value' => [['entity::unique', User::class, $field, $fields]]] + ['value' => [['entity::unique', User::class, $field, $fields, $ignoreCase]]] ); if ($context !== null) { $validator = $validator->withContext($context); diff --git a/tests/app/src/Bootloader/AppBootloader.php b/tests/app/src/Bootloader/AppBootloader.php index 5886ad1fa..ababbca16 100644 --- a/tests/app/src/Bootloader/AppBootloader.php +++ b/tests/app/src/Bootloader/AppBootloader.php @@ -155,6 +155,64 @@ public function boot( new Interceptor\Append('six'), ] ); + + $this->registerInterceptedRoute( + $router, + 'withoutAttribute', + [ + new Interceptor\Append('one'), + new Interceptor\Append('two'), + new Interceptor\Append('three'), + ] + ); + + $this->registerInterceptedRoute( + $router, + 'withAttribute', + [ + $pipelineInterceptor, + ] + ); + $this->registerInterceptedRoute( + $router, + 'mixAttribute', + [ + new Interceptor\Append('four'), + new Interceptor\Append('five'), + $pipelineInterceptor, + new Interceptor\Append('six'), + ] + ); + $this->registerInterceptedRoute( + $router, + 'dupAttribute', + [ + $pipelineInterceptor, + new Interceptor\Append('one'), + new Interceptor\Append('two'), + new Interceptor\Append('three'), + ] + ); + $this->registerInterceptedRoute( + $router, + 'skipAttribute', + [ + new Interceptor\Append('one'), + $pipelineInterceptor, + new Interceptor\Append('two'), + new Interceptor\Append('three'), + ] + ); + $this->registerInterceptedRoute( + $router, + 'firstAttribute', + [ + $pipelineInterceptor, + new Interceptor\Append('four'), + new Interceptor\Append('five'), + new Interceptor\Append('six'), + ] + ); } private function registerInterceptedRoute(RouterInterface $router, string $action, array $interceptors): void diff --git a/tests/app/src/Controller/Demo2Controller.php b/tests/app/src/Controller/Demo2Controller.php index a15e034af..9180f2dcd 100644 --- a/tests/app/src/Controller/Demo2Controller.php +++ b/tests/app/src/Controller/Demo2Controller.php @@ -27,6 +27,12 @@ public function do1() return 'ok'; } + #[Guarded('do')] + public function do1Attribute() + { + return 'ok'; + } + /** * @Guarded("do", else="notFound") */ diff --git a/tests/app/src/Controller/DemoController.php b/tests/app/src/Controller/DemoController.php index 46cee7be8..5f2e3850e 100644 --- a/tests/app/src/Controller/DemoController.php +++ b/tests/app/src/Controller/DemoController.php @@ -36,6 +36,15 @@ public function guardedButNoName() return 'ok'; } + /** + * @return string + */ + #[Guarded()] + public function guardedButNoNameAttribute() + { + return 'ok'; + } + /** * @Guarded("do") * @return string @@ -44,4 +53,13 @@ public function do() { return 'ok'; } + + /** + * @return string + */ + #[Guarded(permission: 'do')] + public function doAttribute() + { + return 'ok'; + } } diff --git a/tests/app/src/Controller/InterceptedController.php b/tests/app/src/Controller/InterceptedController.php index 6ea7b6449..5c494d485 100644 --- a/tests/app/src/Controller/InterceptedController.php +++ b/tests/app/src/Controller/InterceptedController.php @@ -58,4 +58,49 @@ public function first(): array { return [__FUNCTION__]; } + + /** + * @return array + */ + #[Pipeline(pipeline: [Interceptor\One::class, Interceptor\Two::class, Interceptor\Three::class])] + public function withAttribute(): array + { + return [__FUNCTION__]; + } + + /** + * @return array + */ + #[Pipeline(pipeline: [Interceptor\One::class, Interceptor\Two::class, Interceptor\Three::class])] + public function mixAttribute(): array + { + return [__FUNCTION__]; + } + + /** + * @return array + */ + #[Pipeline(pipeline: [Interceptor\One::class, Interceptor\Two::class, Interceptor\Three::class])] + public function dupAttribute(): array + { + return [__FUNCTION__]; + } + + /** + * @return array + */ + #[Pipeline(pipeline: [Interceptor\One::class, Interceptor\Two::class, Interceptor\Three::class], skipNext: true)] + public function skipAttribute(): array + { + return [__FUNCTION__]; + } + + /** + * @return array + */ + #[Pipeline([Interceptor\One::class, Interceptor\Two::class, Interceptor\Three::class], true)] + public function firstAttribute(): array + { + return [__FUNCTION__]; + } }