diff --git a/benchmarks/Routing/RouteParserBench.php b/benchmarks/Routing/RouteParserBench.php new file mode 100644 index 000000000..e6c72149a --- /dev/null +++ b/benchmarks/Routing/RouteParserBench.php @@ -0,0 +1,10 @@ + ./src/Viserio/Queue/Tests - + ./src/Viserio/Session/Tests diff --git a/src/Viserio/Bus/Dispatcher.php b/src/Viserio/Bus/Dispatcher.php index 66cb6ee70..f284f5d18 100644 --- a/src/Viserio/Bus/Dispatcher.php +++ b/src/Viserio/Bus/Dispatcher.php @@ -8,7 +8,7 @@ use Viserio\Contracts\Bus\Dispatcher as DispatcherContract; use Viserio\Pipeline\Pipeline; use Viserio\Support\Invoker; -use Viserio\Support\Traits\ContainerAwareTrait; +use Viserio\Contracts\Container\Traits\ContainerAwareTrait; class Dispatcher implements DispatcherContract { diff --git a/src/Viserio/Cache/CacheManager.php b/src/Viserio/Cache/CacheManager.php index 42c78afad..2a20f1070 100644 --- a/src/Viserio/Cache/CacheManager.php +++ b/src/Viserio/Cache/CacheManager.php @@ -23,7 +23,6 @@ use Memcached; use MongoDB\Driver\Manager as MongoDBManager; use Predis\Client as PredisClient; -use Psr\Cache\CacheItemPoolInterface; use Redis; use Viserio\{ Filesystem\FilesystemManager, diff --git a/src/Viserio/Cache/composer.json b/src/Viserio/Cache/composer.json index 16aae3b90..7f18388b5 100644 --- a/src/Viserio/Cache/composer.json +++ b/src/Viserio/Cache/composer.json @@ -39,16 +39,16 @@ ], "require": { "php" : "7.0.0 - 7.0.5 || ^7.0.7", - "cache/chain-adapter" : "^0.3", + "cache/chain-adapter" : "^0.4", "cache/namespaced-cache" : "^0.1", "psr/cache" : "^1.0", "viserio/config" : "self.version", "viserio/cotracts" : "self.version" }, "require-dev": { - "cache/array-adapter" : "^0.2", + "cache/array-adapter" : "^0.4", "cache/filesystem-adapter" : "^0.3", - "cache/session-handler" : "^0.1", + "cache/session-handler" : "^0.2", "cache/void-adapter" : "^0.3", "narrowspark/php-cs-fixer-config" : "^1.1", "narrowspark/testing-helper" : "^1.5", @@ -76,13 +76,13 @@ "suggest": { "cache/apc-adapter" : "Required to use the Apc cache (^0.3).", "cache/apcu-adapter" : "Required to use the Apcu cache (^0.3).", - "cache/array-adapter" : "Required to use the Array cache (^0.2)", + "cache/array-adapter" : "Required to use the Array cache (^0.4)", "cache/filesystem-adapter" : "Required to use the Filesystem cache (^0.3).", "cache/memcache-adapter" : "Required to use the Memcache cache (^0.3).", "cache/memcached-adapter" : "Required to use the Memcached cache (^0.3).", "cache/mongodb-adapter" : "Required to use the Mongodb cache (^0.2).", "cache/predis-adapter" : "Required to use the Predis cache (^0.4).", - "cache/session-handler" : "Required to use the Session cache (^0.1).", + "cache/session-handler" : "Required to use the Session cache (^0.2).", "cache/void-adapter" : "Required to use the Void cache (^0.3)." }, "minimum-stability" : "dev", diff --git a/src/Viserio/Connect/ConnectManager.php b/src/Viserio/Connect/ConnectManager.php index 0c77829f8..4ee1d4320 100644 --- a/src/Viserio/Connect/ConnectManager.php +++ b/src/Viserio/Connect/ConnectManager.php @@ -106,9 +106,9 @@ protected function createMariadbConnection(array $config): PDO * * @param array $config * - * @return \Mongo|\MongoClient|\MongoDB\Client + * @return \Mongo */ - protected function createMongoConnection(array $config): PDO + protected function createMongoConnection(array $config) { return (new MongoConnector())->connect($config); } diff --git a/src/Viserio/Console/Application.php b/src/Viserio/Console/Application.php index a1d217b4d..3aacdde75 100644 --- a/src/Viserio/Console/Application.php +++ b/src/Viserio/Console/Application.php @@ -2,20 +2,27 @@ declare(strict_types=1); namespace Viserio\Console; +use Closure; use Interop\Container\ContainerInterface as ContainerContract; use Invoker\Exception\InvocationException; use RuntimeException; -use Symfony\Component\Console\Application as SymfonyConsole; -use Symfony\Component\Console\Command\Command as SymfonyCommand; -use Symfony\Component\Console\Input\InputDefinition; -use Symfony\Component\Console\Input\InputInterface; -use Symfony\Component\Console\Output\OutputInterface; -use Viserio\Console\Command\Command as ViserioCommand; -use Viserio\Console\Command\ExpressionParser as Parser; -use Viserio\Console\Input\InputOption; -use Viserio\Contracts\Console\Application as ApplicationContract; +use Symfony\Component\Console\{ + Application as SymfonyConsole, + Command\Command as SymfonyCommand, + Input\InputDefinition, + Input\InputInterface, + Output\OutputInterface +}; +use Viserio\Console\{ + Command\Command as ViserioCommand, + Command\ExpressionParser as Parser, + Input\InputOption +}; +use Viserio\Contracts\{ + Console\Application as ApplicationContract, + Container\Traits\ContainerAwareTrait +}; use Viserio\Support\Invoker; -use Viserio\Support\Traits\ContainerAwareTrait; class Application extends SymfonyConsole implements ApplicationContract { @@ -59,9 +66,9 @@ class Application extends SymfonyConsole implements ApplicationContract /** * Create a new Cerebro console application. * - * @param ContainerContract $container - * @param string $version - * @param string $name + * @param \Interop\Container\ContainerInterface $container + * @param string $version + * @param string $name */ public function __construct( ContainerContract $container, @@ -70,15 +77,14 @@ public function __construct( ) { $this->name = $name; $this->version = $version; - - $this->setContainer($container); + $this->container = $container; $this->expressionParser = new Parser(); - $this->initInvoker(); + $this->initInvoker(); $this->setAutoExit(false); $this->setCatchExceptions(false); - parent::__construct($this->getName(), $this->getVersion()); + parent::__construct($name, $version); } /** @@ -86,7 +92,7 @@ public function __construct( * * @param \Symfony\Component\Console\Command\Command $command * - * @return SymfonyCommand|null + * @return null|\Symfony\Component\Console\Command\Command */ public function add(SymfonyCommand $command) { @@ -105,10 +111,11 @@ public function add(SymfonyCommand $command) * @param callable|string|array $callable Called when the command is called. * When using a container, this can be a "pseudo-callable" * i.e. the name of the container entry to invoke. + * @param array $aliases An array of aliases for the command. * - * @return SymfonyCommand + * @return \Symfony\Component\Console\Command\Command */ - public function command(string $expression, $callable): SymfonyCommand + public function command(string $expression, $callable, array $aliases = []): SymfonyCommand { $commandFunction = function (InputInterface $input, OutputInterface $output) use ($callable) { $parameters = array_merge( @@ -120,18 +127,23 @@ public function command(string $expression, $callable): SymfonyCommand $input->getOptions() ); + if ($callable instanceof Closure) { + $callable = $callable->bindTo($this, $this); + } + try { $this->getInvoker()->call($callable, $parameters); - } catch (InvocationException $e) { + } catch (InvocationException $exception) { throw new RuntimeException(sprintf( "Impossible to call the '%s' command: %s", $input->getFirstArgument(), - $e->getMessage() - ), 0, $e); + $exception->getMessage() + ), 0, $exception); } }; $command = $this->createCommand($expression, $commandFunction); + $command->setAliases($aliases); $this->add($command); @@ -208,7 +220,7 @@ protected function getEnvironmentOption(): InputOption * @param string $expression * @param callable $callable * - * @return SymfonyCommand + * @return \Symfony\Component\Console\Command\Command */ protected function createCommand(string $expression, callable $callable): SymfonyCommand { diff --git a/src/Viserio/Console/Command/Command.php b/src/Viserio/Console/Command/Command.php index bfba1392a..c5ee58e49 100644 --- a/src/Viserio/Console/Command/Command.php +++ b/src/Viserio/Console/Command/Command.php @@ -18,11 +18,11 @@ Question\Question }; use Viserio\Console\Style\NarrowsparkStyle; -use Viserio\Contracts\Support\Arrayable; -use Viserio\Support\{ - Invoker, - Traits\ContainerAwareTrait +use Viserio\Contracts\{ + Support\Arrayable, + Container\Traits\ContainerAwareTrait }; +use Viserio\Support\Invoker; abstract class Command extends BaseCommand implements CompletionAwareInterface { diff --git a/src/Viserio/Console/Command/ExpressionParser.php b/src/Viserio/Console/Command/ExpressionParser.php index cae7939a2..dc6713865 100644 --- a/src/Viserio/Console/Command/ExpressionParser.php +++ b/src/Viserio/Console/Command/ExpressionParser.php @@ -2,9 +2,11 @@ declare(strict_types=1); namespace Viserio\Console\Command; -use Viserio\Console\Input\InputArgument; -use Viserio\Console\Input\InputOption; -use Viserio\Contracts\Console\InvalidCommandExpression; +use Viserio\Console\Input\{ + InputArgument, + InputOption +}; +use Viserio\Contracts\Console\Exceptions\InvalidCommandExpression; use Viserio\Support\Str; class ExpressionParser diff --git a/src/Viserio/Console/Tests/ApplicationTest.php b/src/Viserio/Console/Tests/ApplicationTest.php index b254f556b..93e679f0a 100644 --- a/src/Viserio/Console/Tests/ApplicationTest.php +++ b/src/Viserio/Console/Tests/ApplicationTest.php @@ -4,12 +4,16 @@ use Mockery as Mock; use Narrowspark\TestingHelper\ArrayContainer; -use stdClass; -use Symfony\Component\Console\Input\StringInput; -use Symfony\Component\Console\Output\OutputInterface; -use Viserio\Console\Application; -use Viserio\Console\Tests\Fixture\SpyOutput; -use Viserio\Console\Tests\Fixture\ViserioCommand; +use StdClass; +use Symfony\Component\Console\{ + Input\StringInput, + Output\OutputInterface +}; +use Viserio\Console\{ + Application, + Tests\Fixture\SpyOutput, + Tests\Fixture\ViserioCommand +}; class ApplicationTest extends \PHPUnit_Framework_TestCase { @@ -20,9 +24,9 @@ class ApplicationTest extends \PHPUnit_Framework_TestCase public function setUp() { - $stdClass = new stdClass(); + $stdClass = new StdClass(); $stdClass->foo = 'hello'; - $stdClass2 = new stdClass(); + $stdClass2 = new StdClass(); $stdClass2->foo = 'nope!'; $container = new ArrayContainer([ @@ -139,7 +143,7 @@ public function testItShouldRunACommandWitMultipleOptions() public function testItShouldInjectTypeHintInPriority() { - $this->application->command('greet', function (OutputInterface $output, stdClass $param) { + $this->application->command('greet', function (OutputInterface $output, StdClass $param) { $output->write($param->foo); }); @@ -149,20 +153,23 @@ public function testItShouldInjectTypeHintInPriority() public function testItCanResolveCallableStringFromContainer() { $this->application->command('greet', 'command.greet'); + $this->assertOutputIs('greet', 'hello'); } public function testItCanResolveCallableArrayFromContainer() { $this->application->command('greet', 'command.arr.greet'); + $this->assertOutputIs('greet', 'hello'); } public function testItcanInjectUsingTypeHints() { - $this->application->command('greet', function (OutputInterface $output, stdClass $stdClass) { + $this->application->command('greet', function (OutputInterface $output, StdClass $stdClass) { $output->write($stdClass->foo); }); + $this->assertOutputIs('greet', 'hello'); } @@ -171,6 +178,7 @@ public function testItCanInjectUsingParameterNames() $this->application->command('greet', function (OutputInterface $output, $stdClass) { $output->write($stdClass->foo); }); + $this->assertOutputIs('greet', 'hello'); } @@ -182,9 +190,31 @@ public function testItShouldThrowIfAParameterCannotBeResolved() { $this->application->command('greet', function ($fbo) { }); + $this->assertOutputIs('greet', ''); } + public function testRunsACommandViaItsAliasAndReturnsExitCode() + { + $this->application->command('foo', function ($output) { + $output->write(1); + }, ['bar']); + + $this->assertOutputIs('bar', 1); + } + + public function testitShouldRunACommandInTheScopeOfTheApplication() + { + $whatIsThis = null; + + $this->application->command('foo', function () use (&$whatIsThis) { + $whatIsThis = $this; + }); + + $this->assertOutputIs('foo', ''); + $this->assertSame($this->application, $whatIsThis); + } + /** * Fixture method. * @@ -204,6 +234,7 @@ private function assertOutputIs($command, $expected) $output = new SpyOutput(); $this->application->run(new StringInput($command), $output); + $this->assertEquals($expected, $output->output); } } diff --git a/src/Viserio/Console/Tests/Command/CommandTest.php b/src/Viserio/Console/Tests/Command/CommandTest.php index a55209607..a7c4b7de8 100644 --- a/src/Viserio/Console/Tests/Command/CommandTest.php +++ b/src/Viserio/Console/Tests/Command/CommandTest.php @@ -2,17 +2,26 @@ declare(strict_types=1); namespace Viserio\Console\Tests\Command; -use Mockery as Mock; -use Narrowspark\TestingHelper\ArrayContainer; -use Symfony\Component\Console\Input\StringInput; -use Symfony\Component\Console\Output\NullOutput; -use Symfony\Component\Console\Output\OutputInterface; -use Viserio\Console\Application; -use Viserio\Console\Tests\Fixture\ViserioSecCommand as ViserioCommand; +use Narrowspark\TestingHelper\{ + ArrayContainer, + Traits\MockeryTrait +}; +use Symfony\Component\Console\{ + Input\StringInput, + Output\NullOutput, + Output\OutputInterface +}; +use Viserio\Console\{ + Application, + Tests\Fixture\ViserioCommand, + Tests\Fixture\ViserioSecCommand +}; use Viserio\Support\Invoker; class CommandTest extends \PHPUnit_Framework_TestCase { + use MockeryTrait; + /** * @var Application */ @@ -25,6 +34,8 @@ class CommandTest extends \PHPUnit_Framework_TestCase public function setUp() { + parent::setUp(); + $container = new ArrayContainer([ 'foo' => function (OutputInterface $output) { $output->write('hello'); @@ -39,62 +50,31 @@ public function setUp() ->setContainer($this->application->getContainer()); } - public function tearDown() - { - Mock::close(); - } - public function testGetNormalVerbosity() { - $command = new ViserioCommand(); + $command = new ViserioSecCommand(); $this->assertSame(32, $command->getVerbosity()); } public function testGetVerbosityLevelFromCommand() { - $command = new ViserioCommand(); + $command = new ViserioSecCommand(); $this->assertSame(128, $command->getVerbosity(128)); - $command = new ViserioCommand(); + $command = new ViserioSecCommand(); $this->assertSame(128, $command->getVerbosity('vv')); } public function testSetVerbosityLevelToCommand() { - $command = new ViserioCommand(); + $command = new ViserioSecCommand(); $command->setVerbosity(256); $this->assertSame(256, $command->getVerbosity()); } - // @TODO finish test. - // public function testCallAnotherConsoleCommand() - // { - // $container = new MockContainer(); - // $events = Mock::mock('Viserio\Contracts\Events\Dispatcher', ['addListener' => null]); - // - // $application = new Application($container, $events, '1.0.0'); - // $application->command('foo', function (OutputInterface $output) { - // $output->write('hello'); - // }); - // - // $command = new ViserioCommand(); - // $command->setApplication($application); - // $command->setInvoker( - // (new Invoker()) - // ->injectByTypeHint(true) - // ->injectByParameterName(true) - // ->setContainer($application->getContainer()) - // ); - // $command->run(new StringInput(''), new NullOutput()); - // - // $tester = new CommandTester($command); - // - // $this->assertSame($application->get('foo'), $command->call('foo')); - // } - public function testGetOptionFromCommand() { - $command = new ViserioCommand(); + $command = new ViserioSecCommand(); $command->setApplication($this->application); $command->setInvoker($this->invoker); @@ -106,7 +86,7 @@ public function testGetOptionFromCommand() public function testGetArgumentFromCommand() { - $command = new ViserioCommand(); + $command = new ViserioSecCommand(); $command->setApplication($this->application); $command->setInvoker($this->invoker); diff --git a/src/Viserio/Console/Tests/Command/ExpressionParserTest.php b/src/Viserio/Console/Tests/Command/ExpressionParserTest.php index 367d83a49..1317c0460 100644 --- a/src/Viserio/Console/Tests/Command/ExpressionParserTest.php +++ b/src/Viserio/Console/Tests/Command/ExpressionParserTest.php @@ -3,8 +3,10 @@ namespace Viserio\Console\Tests\Command; use Viserio\Console\Command\ExpressionParser; -use Viserio\Console\Input\InputArgument; -use Viserio\Console\Input\InputOption; +use Viserio\Console\Input\{ + InputArgument, + InputOption +}; class ExpressionParserTest extends \PHPUnit_Framework_TestCase { @@ -119,7 +121,7 @@ public function testItParsesOptionsWithShortcuts() } /** - * @expectedException \Viserio\Contracts\Console\InvalidCommandExpression + * @expectedException \Viserio\Contracts\Console\Exceptions\InvalidCommandExpression * @expectedExceptionMessage An option must be enclosed by brackets: [--option] */ public function testItProvidesAnErrorMessageOnOptionsMissingBrackets() @@ -129,7 +131,7 @@ public function testItProvidesAnErrorMessageOnOptionsMissingBrackets() } /** - * @expectedException \Viserio\Contracts\Console\InvalidCommandExpression + * @expectedException \Viserio\Contracts\Console\Exceptions\InvalidCommandExpression * @expectedExceptionMessage The expression was empty */ public function testItProvidesAnErrorMessageOnEmpty() diff --git a/src/Viserio/Console/Tests/Fixture/SpyOutput.php b/src/Viserio/Console/Tests/Fixture/SpyOutput.php index f4c358fe1..8e75a342e 100644 --- a/src/Viserio/Console/Tests/Fixture/SpyOutput.php +++ b/src/Viserio/Console/Tests/Fixture/SpyOutput.php @@ -2,8 +2,10 @@ declare(strict_types=1); namespace Viserio\Console\Tests\Fixture; -use Symfony\Component\Console\Output\Output; -use Symfony\Component\Console\Output\OutputInterface; +use Symfony\Component\Console\Output\{ + Output, + OutputInterface +}; class SpyOutput extends Output implements OutputInterface { diff --git a/src/Viserio/Console/Tests/Fixture/ViserioCommand.php b/src/Viserio/Console/Tests/Fixture/ViserioCommand.php index 3c4280442..46bda1ba9 100644 --- a/src/Viserio/Console/Tests/Fixture/ViserioCommand.php +++ b/src/Viserio/Console/Tests/Fixture/ViserioCommand.php @@ -2,8 +2,10 @@ declare(strict_types=1); namespace Viserio\Console\Tests\Fixture; -use Symfony\Component\Console\Input\InputArgument; -use Symfony\Component\Console\Input\InputOption; +use Symfony\Component\Console\Input\{ + InputArgument, + InputOption +}; use Viserio\Console\Command\Command; class ViserioCommand extends Command diff --git a/src/Viserio/Console/Tests/Fixture/ViserioSecCommand.php b/src/Viserio/Console/Tests/Fixture/ViserioSecCommand.php index 09df1c75f..8ef17aae1 100644 --- a/src/Viserio/Console/Tests/Fixture/ViserioSecCommand.php +++ b/src/Viserio/Console/Tests/Fixture/ViserioSecCommand.php @@ -2,8 +2,10 @@ declare(strict_types=1); namespace Viserio\Console\Tests\Fixture; -use Symfony\Component\Console\Input\InputArgument; -use Symfony\Component\Console\Input\InputOption; +use Symfony\Component\Console\Input\{ + InputArgument, + InputOption +}; use Viserio\Console\Command\Command; class ViserioSecCommand extends Command diff --git a/src/Viserio/Console/composer.json b/src/Viserio/Console/composer.json index 1a51afdda..8014e36cd 100644 --- a/src/Viserio/Console/composer.json +++ b/src/Viserio/Console/composer.json @@ -20,6 +20,7 @@ "require": { "php" : "7.0.0 - 7.0.5 || ^7.0.7", "container-interop/container-interop" : "^1.1", + "php-di/invoker" : "^1.3", "stecman/symfony-console-completion" : "^0.6", "symfony/console" : "^3.1", "viserio/cotracts" : "self.version", diff --git a/src/Viserio/Container/Container.php b/src/Viserio/Container/Container.php index 42d1e9e27..179aa8977 100644 --- a/src/Viserio/Container/Container.php +++ b/src/Viserio/Container/Container.php @@ -3,7 +3,6 @@ namespace Viserio\Container; use Interop\Container\ContainerInterface as ContainerInteropInterface; -use Nucleus\Invoker\Invoker; use Viserio\Container\Exception\BindingResolutionException; use Viserio\Container\Exception\ContainerException; use Viserio\Container\Exception\NotFoundException; diff --git a/src/Viserio/Container/ContextualBindingBuilder.php b/src/Viserio/Container/ContextualBindingBuilder.php index ee6a3ec78..6224beef5 100644 --- a/src/Viserio/Container/ContextualBindingBuilder.php +++ b/src/Viserio/Container/ContextualBindingBuilder.php @@ -3,7 +3,7 @@ namespace Viserio\Container; use Viserio\Contracts\Container\ContextualBindingBuilder as ContextualBindingBuilderContract; -use Viserio\Support\Traits\ContainerAwareTrait; +use Viserio\Contracts\Container\Traits\ContainerAwareTrait; /** * ContextualBindingBuilder. diff --git a/src/Viserio/Container/Inflector.php b/src/Viserio/Container/Inflector.php index 14827daf5..84ddd2b6a 100644 --- a/src/Viserio/Container/Inflector.php +++ b/src/Viserio/Container/Inflector.php @@ -2,7 +2,7 @@ declare(strict_types=1); namespace Viserio\Container; -use Viserio\Support\Traits\ContainerAwareTrait; +use Viserio\Contracts\Container\Traits\ContainerAwareTrait; class Inflector { diff --git a/src/Viserio/Contracts/Console/InvalidCommandExpression.php b/src/Viserio/Contracts/Console/Exceptions/InvalidCommandExpression.php similarity index 73% rename from src/Viserio/Contracts/Console/InvalidCommandExpression.php rename to src/Viserio/Contracts/Console/Exceptions/InvalidCommandExpression.php index 6b95fd735..ef0867e0f 100644 --- a/src/Viserio/Contracts/Console/InvalidCommandExpression.php +++ b/src/Viserio/Contracts/Console/Exceptions/InvalidCommandExpression.php @@ -1,6 +1,6 @@ diff --git a/src/Viserio/Contracts/Exception/Handler.php b/src/Viserio/Contracts/Exception/Handler.php index ff962dc00..6e3cc6c8c 100644 --- a/src/Viserio/Contracts/Exception/Handler.php +++ b/src/Viserio/Contracts/Exception/Handler.php @@ -74,11 +74,13 @@ public function addShouldntReport(Throwable $exception): Handler; /** * Register the exception / Error handlers for the application. + * @return void */ public function register(); /** * Unregister the PHP error handler. + * @return void */ public function unregister(); @@ -96,6 +98,7 @@ public function unregister(); * @param null $context * * @throws \ErrorException + * @return void */ public function handleError( int $level, @@ -113,11 +116,13 @@ public function handleError( * be handled differently since they are not normal exceptions. * * @param \Throwable $exception + * @return null|string */ public function handleException(Throwable $exception); /** * Handle the PHP shutdown event. + * @return void */ public function handleShutdown(); } diff --git a/src/Viserio/Contracts/Http/Exceptions/ByteCountingStreamException.php b/src/Viserio/Contracts/Http/Exceptions/ByteCountingStreamException.php new file mode 100644 index 000000000..80188b2c4 --- /dev/null +++ b/src/Viserio/Contracts/Http/Exceptions/ByteCountingStreamException.php @@ -0,0 +1,62 @@ +expectBytes = $expect; + $this->actualBytes = $actual; + + parent::__construct($msg, 0, $previous); + } + + /** + * Get expected bytes to be read. + * + * @return int + */ + public function getExpectBytes(): int + { + return $this->expectBytes; + } + + /** + * Get remaining bytes available for read. + * + * @return int + */ + public function getRemainingBytes(): int + { + return $this->actualBytes; + } +} diff --git a/src/Viserio/Contracts/Mail/GPGMailer.php b/src/Viserio/Contracts/Mail/GPGMailer.php index 64fed0573..3cab9304d 100644 --- a/src/Viserio/Contracts/Mail/GPGMailer.php +++ b/src/Viserio/Contracts/Mail/GPGMailer.php @@ -81,7 +81,7 @@ public function sign($text): string; * * @throws \Exception * - * @return string + * @return boolean */ public function verify($text, string $fingerprint): bool; } diff --git a/src/Viserio/Contracts/Routing/CustomStrategy.php b/src/Viserio/Contracts/Routing/CustomStrategy.php deleted file mode 100644 index eed7ec2a5..000000000 --- a/src/Viserio/Contracts/Routing/CustomStrategy.php +++ /dev/null @@ -1,24 +0,0 @@ - ClassName, 1 => MethodName]) - * - \Closure (controller is an anonymous function) - * - * @param string|array|\Closure $controller - * @param array $vars - named wildcard segments of the matched route - * - * @return mixed - */ - public function dispatch($controller, array $vars); -} diff --git a/src/Viserio/Contracts/Routing/DataGenerator.php b/src/Viserio/Contracts/Routing/DataGenerator.php deleted file mode 100644 index 85c169260..000000000 --- a/src/Viserio/Contracts/Routing/DataGenerator.php +++ /dev/null @@ -1,13 +0,0 @@ - MatchResult::FOUND, 1 => , 2 => ] + * + * [0 => MatchResult::HTTP_METHOD_NOT_ALLOWED, 1 => ] + * + * [0 => MatchResult::NOT_FOUND] + * + * @param string $httpMethod + * @param string $uri + * + * @return array + * + * @throws \RuntimeException + */ + public function dispatch(string $httpMethod, string $uri): array; +} diff --git a/src/Viserio/Contracts/Routing/Exceptions/InvalidRouteDataException.php b/src/Viserio/Contracts/Routing/Exceptions/InvalidRouteDataException.php new file mode 100644 index 000000000..0cc97844f --- /dev/null +++ b/src/Viserio/Contracts/Routing/Exceptions/InvalidRouteDataException.php @@ -0,0 +1,18 @@ +getName(), + $route->getUri() + )); + } +} diff --git a/src/Viserio/Contracts/Routing/Pattern.php b/src/Viserio/Contracts/Routing/Pattern.php new file mode 100644 index 000000000..31ea23c1f --- /dev/null +++ b/src/Viserio/Contracts/Routing/Pattern.php @@ -0,0 +1,28 @@ + 'user' }, + * ParameterSegment{ $name => 'id', $match => '[0-9]+' }, + * StaticSegment{ $value => 'create' }, + * ] + * + * @param string $route + * @param string[] $conditions + * + * @return \Viserio\Contracts\Routing\RouteSegment[] + * + * @throws \Viserio\Contracts\Routing\Exception\InvalidRoutePatternException + */ + public function parse(string $route, array $conditions): array; +} diff --git a/src/Viserio/Contracts/Routing/RouteStrategy.php b/src/Viserio/Contracts/Routing/RouteStrategy.php deleted file mode 100644 index 8112f9ef6..000000000 --- a/src/Viserio/Contracts/Routing/RouteStrategy.php +++ /dev/null @@ -1,13 +0,0 @@ -assertEquals($msg, $exception->getMessage()); + $this->assertSame($prev, $exception->getPrevious()); + } + + public function getTestCases() + { + return [[7, 5], [5, 0]]; + } +} diff --git a/src/Viserio/Contracts/View/Traits/ViewAwareTrait.php b/src/Viserio/Contracts/View/Traits/ViewAwareTrait.php index 6c058d1e9..65a8ea4a3 100644 --- a/src/Viserio/Contracts/View/Traits/ViewAwareTrait.php +++ b/src/Viserio/Contracts/View/Traits/ViewAwareTrait.php @@ -10,7 +10,7 @@ trait ViewAwareTrait /** * View factory instance. * - * @var \Interop\Container\ContainerInterface|null + * @var \Viserio\Contracts\View\Factory */ protected $views; diff --git a/src/Viserio/Contracts/View/Virtuoso.php b/src/Viserio/Contracts/View/Virtuoso.php index 9dd2e3060..cf7e5181a 100644 --- a/src/Viserio/Contracts/View/Virtuoso.php +++ b/src/Viserio/Contracts/View/Virtuoso.php @@ -83,6 +83,7 @@ public function yieldContent(string $section, string $default = ''): string; * * @param string $section * @param string $content + * @return void */ public function startSection(string $section, string $content = ''); @@ -91,6 +92,7 @@ public function startSection(string $section, string $content = ''); * * @param string $section * @param string $content + * @return void */ public function inject(string $section, string $content); @@ -116,21 +118,25 @@ public function appendSection(): string; /** * Clear all of the section contents. + * @return void */ public function clearSections(); /** * Clear all of the section contents if done rendering. + * @return void */ public function clearSectionsIfDoneRendering(); /** * Increment the rendering counter. + * @return void */ public function incrementRender(); /** * Decrement the rendering counter. + * @return void */ public function decrementRender(); diff --git a/src/Viserio/Contracts/composer.json b/src/Viserio/Contracts/composer.json index 1f3890502..52af450b6 100644 --- a/src/Viserio/Contracts/composer.json +++ b/src/Viserio/Contracts/composer.json @@ -7,7 +7,8 @@ "narrowspark", "contracts", "interfaces", - "message", "psr-7", + "message", + "psr-7", "log", "psr-3", "RFC 2616", @@ -28,8 +29,12 @@ } ], "require": { - "php" : "7.0.0 - 7.0.5 || ^7.0.7", - "psr/cache" : "^1.0", + "php" : "7.0.0 - 7.0.5 || ^7.0.7" + }, + "require-dev": { + "phpunit/phpunit" : "^5.1", + "narrowspark/php-cs-fixer-config" : "^1.1", + "narrowspark/testing-helper" : "^1.5", "container-interop/container-interop" : "^1.1", "psr/http-message" : "^1.0", "psr/log" : "^1.0" @@ -40,6 +45,11 @@ }, "exclude-from-classmap" : ["/Tests/"] }, + "autoload-dev": { + "psr-4": { + "Viserio\\Contracts\\Tests\\" : "Tests/" + } + }, "extra": { "branch-alias": { "dev-master" : "1.0-dev" @@ -50,6 +60,11 @@ "psr/http-message-implementation" : "^1.0", "psr/log-implementation" : "^1.0" }, + "suggest": { + "psr/log" : "Required to use log contracts (^1.0).", + "psr/http-message" : "Required to use Http, Middleware, Routing contracts (^1.0).", + "container-interop/container-interop" : "Required to use Container contract (^1.0)." + }, "minimum-stability" : "dev", "prefer-stable" : true } diff --git a/src/Viserio/Contracts/phpunit.xml.dist b/src/Viserio/Contracts/phpunit.xml.dist new file mode 100644 index 000000000..c5e4bab3a --- /dev/null +++ b/src/Viserio/Contracts/phpunit.xml.dist @@ -0,0 +1,40 @@ + + + + + + + + + + + + ./Tests/ + + + + + + ./ + + ./vendor + ./Tests + + + + + diff --git a/src/Viserio/Events/Dispatcher.php b/src/Viserio/Events/Dispatcher.php index 4cb01cb99..1a1b09226 100644 --- a/src/Viserio/Events/Dispatcher.php +++ b/src/Viserio/Events/Dispatcher.php @@ -3,10 +3,14 @@ namespace Viserio\Events; use Interop\Container\ContainerInterface as ContainerContract; -use Viserio\Contracts\Events\Dispatcher as DispatcherContract; -use Viserio\Support\Invoker; -use Viserio\Support\Str; -use Viserio\Support\Traits\ContainerAwareTrait; +use Viserio\Contracts\{ + Container\Traits\ContainerAwareTrait, + Events\Dispatcher as DispatcherContract +}; +use Viserio\Support\{ + Invoker, + Str +}; class Dispatcher implements DispatcherContract { diff --git a/src/Viserio/Events/composer.json b/src/Viserio/Events/composer.json index 981590b56..b5de64cf1 100644 --- a/src/Viserio/Events/composer.json +++ b/src/Viserio/Events/composer.json @@ -19,6 +19,7 @@ ], "require": { "php" : "7.0.0 - 7.0.5 || ^7.0.7", + "danielstjules/stringy" : "^2.3", "viserio/cotracts" : "self.version", "viserio/support" : "self.version" }, diff --git a/src/Viserio/Http/Request.php b/src/Viserio/Http/Request.php index 9e9684318..8c9fb91c7 100644 --- a/src/Viserio/Http/Request.php +++ b/src/Viserio/Http/Request.php @@ -159,7 +159,7 @@ public function getUri() */ public function withUri(UriInterface $uri, $preserveHost = false) { - if ($uri === $this->uri) { + if ($this->uri === $uri) { return $this; } diff --git a/src/Viserio/Http/Stream/AbstractStreamDecorator.php b/src/Viserio/Http/Stream/AbstractStreamDecorator.php index b735f46f4..cfdef4578 100644 --- a/src/Viserio/Http/Stream/AbstractStreamDecorator.php +++ b/src/Viserio/Http/Stream/AbstractStreamDecorator.php @@ -2,39 +2,25 @@ declare(strict_types=1); namespace Viserio\Http\Stream; -use BadMethodCallException; -use Exception; +use Throwable; use Psr\Http\Message\StreamInterface; -use UnexpectedValueException; use Viserio\Http\Util; abstract class AbstractStreamDecorator implements StreamInterface { /** - * @param StreamInterface $stream Stream to decorate + * Stream instance. + * + * @var \Psr\Http\Message\StreamInterface */ - public function __construct(StreamInterface $stream) - { - $this->stream = $stream; - } + protected $stream; /** - * Magic method used to create a new stream if streams are not added in - * the constructor of a decorator (e.g., LazyOpenStream). - * - * @param string $name Name of the property (allows "stream" only). - * - * @return StreamInterface + * @param StreamInterface $stream Stream to decorate */ - public function __get($name) + public function __construct(StreamInterface $stream) { - if ($name == 'stream') { - $this->stream = $this->createStream(); - - return $this->stream; - } - - throw new UnexpectedValueException("$name not found on class"); + $this->stream = $stream; } /** @@ -48,7 +34,7 @@ public function __toString() } return $this->getContents(); - } catch (Exception $e) { + } catch (Throwable $e) { // Really, PHP? https://bugs.php.net/bug.php?id=53648 trigger_error('StreamDecorator::__toString exception: ' . (string) $e, E_USER_ERROR); @@ -121,6 +107,9 @@ public function eof() return $this->stream->eof(); } + /** + * {@inheritdoc} + */ public function tell() { return $this->stream->tell(); @@ -181,16 +170,4 @@ public function write($string) { return $this->stream->write($string); } - - /** - * Implement in subclasses to dynamically create streams when requested. - * - * @throws \BadMethodCallException - * - * @return StreamInterface - */ - protected function createStream(): StreamInterface - { - throw new BadMethodCallException('Not implemented'); - } } diff --git a/src/Viserio/Http/Stream/ByteCountingStream.php b/src/Viserio/Http/Stream/ByteCountingStream.php new file mode 100644 index 000000000..5ed73b752 --- /dev/null +++ b/src/Viserio/Http/Stream/ByteCountingStream.php @@ -0,0 +1,78 @@ +stream = $stream; + + if (!is_int($bytesToRead) || $bytesToRead < 0) { + $msg = 'Bytes to read should be a non-negative integer for ' + . sprintf('ByteCountingStream, got %s.', $bytesToRead); + throw new InvalidArgumentException($msg); + } + + if ($this->stream->getSize() !== null && + $bytesToRead > $this->stream->getSize() + ) { + throw new ByteCountingStreamException( + $bytesToRead, + $this->stream->getSize() + ); + } + + $this->remaining = $bytesToRead; + } + + /** + * {@inheritdoc} + * + * @throws \Viserio\Contracts\Http\Exception\ByteCountingStreamException + */ + public function read($length) + { + if ($this->remaining === 0) { + return ''; + } + + $offset = $this->tell(); + $bytesToRead = min($length, $this->remaining); + $data = $this->stream->read($bytesToRead); + + $this->remaining -= strlen($data); + + if ((!$data || $data === '') && $this->remaining !== 0) { + // hits EOF + $provide = $this->tell() - $offset; + + throw new ByteCountingStreamException($this->remaining, $provide); + } + + return $data; + } +} diff --git a/src/Viserio/Http/Stream/LazyOpenStream.php b/src/Viserio/Http/Stream/LazyOpenStream.php index 0fb0048ae..7e0edccd9 100644 --- a/src/Viserio/Http/Stream/LazyOpenStream.php +++ b/src/Viserio/Http/Stream/LazyOpenStream.php @@ -3,9 +3,11 @@ namespace Viserio\Http\Stream; use Psr\Http\Message\StreamInterface; +use Throwable; +use UnexpectedValueException; use Viserio\Http\Util; -class LazyOpenStream extends AbstractStreamDecorator +class LazyOpenStream implements StreamInterface { /** * @var string @@ -21,16 +23,167 @@ class LazyOpenStream extends AbstractStreamDecorator * @param string $filename File to lazily open * @param string $mode fopen mode to use when opening the stream */ - public function __construct($filename, $mode) + public function __construct(string $filename, string $mode) { $this->filename = $filename; $this->mode = $mode; } + /** + * Magic method used to create a new stream if streams are not added in + * the constructor of LazyOpenStream. + * + * @param string $name Name of the property (allows "stream" only). + * + * @return \Psr\Http\Message\StreamInterface + */ + public function __get($name) + { + if ($name == 'stream') { + $this->stream = $this->createStream(); + + return $this->stream; + } + + throw new UnexpectedValueException(sprintf('%s not found on class', $name)); + } + + /** + * {@inheritdoc} + */ + public function __toString() + { + try { + if ($this->isSeekable()) { + $this->seek(0); + } + + return $this->getContents(); + } catch (Throwable $e) { + // Really, PHP? https://bugs.php.net/bug.php?id=53648 + trigger_error('StreamDecorator::__toString exception: ' + . (string) $e, E_USER_ERROR); + + return ''; + } + } + + /** + * {@inheritdoc} + */ + public function getContents() + { + return Util::copyToString($this); + } + + /** + * {@inheritdoc} + */ + public function close() + { + $this->stream->close(); + } + + /** + * {@inheritdoc} + */ + public function getMetadata($key = null) + { + return $this->stream->getMetadata($key); + } + + /** + * {@inheritdoc} + */ + public function detach() + { + return $this->stream->detach(); + } + + /** + * {@inheritdoc} + */ + public function getSize() + { + return $this->stream->getSize(); + } + + /** + * {@inheritdoc} + */ + public function eof() + { + return $this->stream->eof(); + } + + /** + * {@inheritdoc} + */ + public function tell() + { + return $this->stream->tell(); + } + + /** + * {@inheritdoc} + */ + public function isReadable() + { + return $this->stream->isReadable(); + } + + /** + * {@inheritdoc} + */ + public function isWritable() + { + return $this->stream->isWritable(); + } + + /** + * {@inheritdoc} + */ + public function isSeekable() + { + return $this->stream->isSeekable(); + } + + /** + * {@inheritdoc} + */ + public function rewind() + { + $this->seek(0); + } + + /** + * {@inheritdoc} + */ + public function seek($offset, $whence = SEEK_SET) + { + $this->stream->seek($offset, $whence); + } + + /** + * {@inheritdoc} + */ + public function read($length) + { + return $this->stream->read($length); + } + + /** + * {@inheritdoc} + */ + public function write($string) + { + return $this->stream->write($string); + } + /** * Creates the underlying stream lazily when required. * - * @return StreamInterface + * @return \Psr\Http\Message\StreamInterface */ protected function createStream(): StreamInterface { diff --git a/src/Viserio/Http/Tests/Stream/ByteCountingStreamTest.php b/src/Viserio/Http/Tests/Stream/ByteCountingStreamTest.php new file mode 100644 index 000000000..d89c06c77 --- /dev/null +++ b/src/Viserio/Http/Tests/Stream/ByteCountingStreamTest.php @@ -0,0 +1,67 @@ +assertEquals('foo ', $testStream->read(4)); + $this->assertEquals('bar ', $testStream->read(4)); + $this->assertEquals('', $testStream->read(4)); + + $testStream->close(); + $testStream = new ByteCountingStream(Util::getStream('testing'), 5); + $testStream->seek(4); + + $this->assertEquals('ing', $testStream->read(5)); + + $testStream->close(); + } + + /** + * @expectedException \Viserio\Contracts\Http\Exceptions\ByteCountingStreamException + * @expectedExceptionMessage The ByteCountingStream decorator expects to be able to read + */ + public function testEnsureStopReadWhenHitEof() + { + $testStream = new ByteCountingStream(Util::getStream('abc'), 3); + $testStream->seek(3); + $testStream->read(3); + } + + /** + * @expectedException \RuntimeException + * @expectedExceptionMessage The stream is detached + */ + public function testEnsureReadUnclosedStream() + { + $body = Util::getStream("closed"); + $closedStream = new ByteCountingStream($body, 5); + $body->close(); + $closedStream->read(3); + } +} diff --git a/src/Viserio/Mail/Mailer.php b/src/Viserio/Mail/Mailer.php index 978d4722c..c76e70253 100644 --- a/src/Viserio/Mail/Mailer.php +++ b/src/Viserio/Mail/Mailer.php @@ -3,12 +3,11 @@ namespace Viserio\Mail; use Closure; -use Exception; use InvalidArgumentException; use Narrowspark\Arr\StaticArr as Arr; use Swift_Mailer; -use Swift_Mime_Message; use Swift_Message; +use Swift_Mime_Message; use Viserio\Contracts\{ Events\Traits\EventsAwareTrait, Mail\Mailer as MailerContract, diff --git a/src/Viserio/Mail/QueueMailer.php b/src/Viserio/Mail/QueueMailer.php index 8631f5b01..01bc51f69 100644 --- a/src/Viserio/Mail/QueueMailer.php +++ b/src/Viserio/Mail/QueueMailer.php @@ -7,6 +7,7 @@ use SuperClosure\Serializer; use Swift_Mailer; use Viserio\Contracts\{ + Container\Traits\ContainerAwareTrait, Mail\QueueMailer as QueueMailerContract, Queue\Queue as QueueContract, Queue\Job as JobContract, @@ -14,7 +15,6 @@ }; use Viserio\Support\{ Invoker, - Traits\ContainerAwareTrait, Str }; diff --git a/src/Viserio/Mail/Transport/Mandrill.php b/src/Viserio/Mail/Transport/Mandrill.php index 3ea434c93..37ca3a90a 100644 --- a/src/Viserio/Mail/Transport/Mandrill.php +++ b/src/Viserio/Mail/Transport/Mandrill.php @@ -69,7 +69,7 @@ public function getKey(): string * * @param string $key * - * @return string + * @return Mandrill */ public function setKey(string $key): Mandrill { diff --git a/src/Viserio/Mail/Transport/Postmark.php b/src/Viserio/Mail/Transport/Postmark.php index fd57a7b45..6eb364c86 100644 --- a/src/Viserio/Mail/Transport/Postmark.php +++ b/src/Viserio/Mail/Transport/Postmark.php @@ -77,7 +77,7 @@ public function getServerToken(): string * * @param string $serverToken * - * @return string + * @return Postmark */ public function setServerToken(string $serverToken): Postmark { @@ -90,7 +90,7 @@ public function setServerToken(string $serverToken): Postmark * Convert email dictionary with emails and names * to array of emails with names. * - * @param array $emails + * @param string[] $emails * * @return array */ @@ -115,7 +115,7 @@ protected function convertEmailsArray(array $emails): array * @param Swift_Mime_Message $message * @param string $mimeType * - * @return \Swift_Mime_MimePart|null + * @return \Swift_Mime_MimeEntity|null */ protected function getMIMEPart(Swift_Mime_Message $message, $mimeType) { diff --git a/src/Viserio/Mail/Transport/Ses.php b/src/Viserio/Mail/Transport/Ses.php index ccfa49145..45aa34628 100644 --- a/src/Viserio/Mail/Transport/Ses.php +++ b/src/Viserio/Mail/Transport/Ses.php @@ -30,7 +30,7 @@ public function __construct(SesClient $ses) * @param \Swift_Mime_Message $message * @param string[]|null $failedRecipients * - * @return Log|null + * @return integer */ public function send(Swift_Mime_Message $message, &$failedRecipients = null) { diff --git a/src/Viserio/Mail/Transport/SparkPost.php b/src/Viserio/Mail/Transport/SparkPost.php index e47ef6ca6..e88a76fa5 100644 --- a/src/Viserio/Mail/Transport/SparkPost.php +++ b/src/Viserio/Mail/Transport/SparkPost.php @@ -89,7 +89,7 @@ public function getKey(): string * * @param string $key * - * @return string + * @return SparkPost */ public function setKey(string $key): SparkPost { diff --git a/src/Viserio/Mail/TransportManager.php b/src/Viserio/Mail/TransportManager.php index b7ffa281d..8dec4a772 100644 --- a/src/Viserio/Mail/TransportManager.php +++ b/src/Viserio/Mail/TransportManager.php @@ -3,12 +3,11 @@ namespace Viserio\Mail; use Aws\Ses\SesClient; -use Interop\Container\ContainerInterface; -use Swift_SmtpTransport as SmtpTransport; -use Swift_MailTransport as MailTransport; -use Narrowspark\Arr\StaticArr as Arr; use GuzzleHttp\Client as HttpClient; +use Narrowspark\Arr\StaticArr as Arr; use Psr\Log\LoggerInterface; +use Swift_MailTransport as MailTransport; +use Swift_SmtpTransport as SmtpTransport; use Viserio\Support\AbstractManager; use Viserio\Mail\Transport\{ Log as LogTransport, diff --git a/src/Viserio/Middleware/Dispatcher.php b/src/Viserio/Middleware/Dispatcher.php index f9768c0dc..dd435b463 100644 --- a/src/Viserio/Middleware/Dispatcher.php +++ b/src/Viserio/Middleware/Dispatcher.php @@ -13,7 +13,7 @@ Middleware\Middleware as MiddlewareContract, Middleware\Stack as StackContract }; -use Viserio\Support\Traits\ContainerAwareTrait; +use Viserio\Contracts\Container\Traits\ContainerAwareTrait; class Dispatcher implements StackContract { @@ -33,6 +33,11 @@ class Dispatcher implements StackContract */ protected $response; + /** + * Create a new middleware instance. + * + * @param \Psr\Http\Message\ResponseInterface $response + */ public function __construct(ResponseInterface $response) { $this->response = $response; diff --git a/src/Viserio/Pipeline/Pipeline.php b/src/Viserio/Pipeline/Pipeline.php index d93985ed5..efeebb1e6 100644 --- a/src/Viserio/Pipeline/Pipeline.php +++ b/src/Viserio/Pipeline/Pipeline.php @@ -4,9 +4,11 @@ use Closure; use ReflectionClass; -use Viserio\Contracts\Pipeline\Pipeline as PipelineContract; +use Viserio\Contracts\{ + Container\Traits\ContainerAwareTrait, + Pipeline\Pipeline as PipelineContract +}; use Viserio\Support\Invoker; -use Viserio\Support\Traits\ContainerAwareTrait; class Pipeline implements PipelineContract { @@ -71,15 +73,9 @@ public function then(Closure $destination) $firstSlice = $this->getInitialSlice($destination); $stages = array_reverse($this->stages); + $callable = array_reduce($stages, $this->getSlice(), $firstSlice); - return call_user_func( - array_reduce( - $stages, - $this->getSlice(), - $firstSlice - ), - $this->traveler - ); + return $callable($this->traveler); } /** @@ -93,7 +89,7 @@ protected function getSlice(): Closure return function ($traveler) use ($stack, $stage) { // If the $stage is an instance of a Closure, we will just call it directly. if ($stage instanceof Closure) { - return call_user_func($stage, $traveler, $stack); + return $stage($traveler, $stack); // Otherwise we'll resolve the stages out of the container and call it with // the appropriate method and arguments, returning the results back out. @@ -101,17 +97,17 @@ protected function getSlice(): Closure return $this->sliceThroughContainer($traveler, $stack, $stage); } elseif (is_array($stage)) { $reflectionClass = new ReflectionClass(array_shift($stage)); + $parameters = [$traveler, $stack]; - return call_user_func_array( - $reflectionClass->newInstanceArgs($stage), - [$traveler, $stack] - ); + return $reflectionClass->newInstanceArgs($stage)(...$parameters); } // If the pipe is already an object we'll just make a callable and pass it to // the pipe as-is. There is no need to do any extra parsing and formatting // since the object we're given was already a fully instantiated object. - return call_user_func_array([$stage, $this->method], [$traveler, $stack]); + $parameters = [$traveler, $stack]; + + return $stage->{$this->method}(...$parameters); }; }; } @@ -126,7 +122,7 @@ protected function getSlice(): Closure protected function getInitialSlice(Closure $destination): Closure { return function ($traveler) use ($destination) { - return call_user_func($destination, $traveler); + return $destination($traveler); }; } diff --git a/src/Viserio/Queue/Connectors/AbstractQueue.php b/src/Viserio/Queue/Connectors/AbstractQueue.php index 81ba034dd..776b4cc21 100644 --- a/src/Viserio/Queue/Connectors/AbstractQueue.php +++ b/src/Viserio/Queue/Connectors/AbstractQueue.php @@ -14,7 +14,7 @@ CallQueuedHandler, QueueClosure }; -use Viserio\Support\Traits\ContainerAwareTrait; +use Viserio\Contracts\Container\Traits\ContainerAwareTrait; abstract class AbstractQueue implements QueueConnectorContract { diff --git a/src/Viserio/Queue/Connectors/SyncQueue.php b/src/Viserio/Queue/Connectors/SyncQueue.php index 32272c73c..0bf1f6ac1 100644 --- a/src/Viserio/Queue/Connectors/SyncQueue.php +++ b/src/Viserio/Queue/Connectors/SyncQueue.php @@ -3,8 +3,8 @@ namespace Viserio\Queue\Connectors; use Throwable; -use Viserio\Contracts\Queue\Job as JobContract; use Viserio\Contracts\Exception\Exception\FatalThrowableError; +use Viserio\Contracts\Queue\Job as JobContract; use Viserio\Queue\Jobs\SyncJob; class SyncQueue extends AbstractQueue diff --git a/src/Viserio/Queue/Jobs/AbstractJob.php b/src/Viserio/Queue/Jobs/AbstractJob.php index 9914ab703..6ce7d4bea 100644 --- a/src/Viserio/Queue/Jobs/AbstractJob.php +++ b/src/Viserio/Queue/Jobs/AbstractJob.php @@ -4,9 +4,11 @@ use DateTime; use Narrowspark\Arr\StaticArr as Arr; -use Viserio\Contracts\Queue\Job as JobContract; +use Viserio\Contracts\{ + Container\Traits\ContainerAwareTrait, + Queue\Job as JobContract +}; use Viserio\Queue\CallQueuedHandler; -use Viserio\Support\Traits\ContainerAwareTrait; abstract class AbstractJob implements JobContract { diff --git a/src/Viserio/Queue/Tests/Fixture/TestQueue.php b/src/Viserio/Queue/Tests/Fixture/TestQueue.php index 8227e4815..6a7f35d0b 100644 --- a/src/Viserio/Queue/Tests/Fixture/TestQueue.php +++ b/src/Viserio/Queue/Tests/Fixture/TestQueue.php @@ -3,7 +3,7 @@ namespace Viserio\Queue\Tests\Fixture; use Viserio\Contracts\Encryption\Encrypter as EncrypterContract; -use Viserio\Support\Traits\ContainerAwareTrait; +use Viserio\Contracts\Container\Traits\ContainerAwareTrait; class TestQueue { diff --git a/src/Viserio/Routing/AbstractController.php b/src/Viserio/Routing/AbstractController.php new file mode 100644 index 000000000..f85a6e6c1 --- /dev/null +++ b/src/Viserio/Routing/AbstractController.php @@ -0,0 +1,8 @@ +container = $container; - $this->routes = $routes; - - parent::__construct($data); + $this->routes = new RouteCollection; } /** - * Match and dispatch a route matching the given http method and uri. + * Match and dispatch a route matching the given http method and + * uri, retruning an execution chain. * - * @param string $method - * @param string $uri + * @param \Psr\Http\Message\ServerRequestInterface $request * - * @return ResponseContract + * @return mixed */ - public function dispatch($method, $uri) + public function handle(ServerRequestInterface $request) { - $match = parent::dispatch($method, $uri); + $match = $this->dispatch( + $request->getMethod(), + $request->getUri()->getPath() + ); switch ($match[0]) { - case FastDispatcher::NOT_FOUND: - return $this->handleNotFound(); - - case FastDispatcher::METHOD_NOT_ALLOWED: - $allowed = (array) $match[1]; - - return $this->handleNotAllowed($allowed); - - case FastDispatcher::FOUND: - default: - $handler = (isset($this->routes[$match[1]]['callback'])) ? - $this->routes[$match[1]]['callback'] : - $match[1]; - - $strategy = $this->routes[$match[1]]['strategy']; - $vars = (array) $match[2]; - - return $this->handleFound($handler, $strategy, $vars); - } - } - - /** - * Invoke a controller action. - * - * @param ResponseContract $controller - * @param array $vars - * - * @return ResponseContract - */ - public function invokeController($controller, array $vars = []) - { - if (is_array($controller)) { - $controller = [ - $this->container[$controller[0]], - $controller[1], - ]; - } - - return call_user_func_array($controller, array_values($vars)); - } - - /** - * Handle dispatching of a found route. - * - * @param string|\Closure $handler - * @param int|\Viserio\Contracts\Routing\CustomStrategy $strategy - * @param array $vars - * - * @throws \RuntimeException - * - * @return ResponseContract - */ - protected function handleFound($handler, $strategy, array $vars = []) - { - if ($this->getStrategy() === null) { - $this->setStrategy($strategy); - } - - $controller = $this->isController($handler); - - // handle getting of response based on strategy - if (is_int($strategy)) { - return $this->getResponseOnStrategy($controller, $strategy, $vars); - } - - $traits = class_uses($strategy, true); - - // dispatch via strategy - if (isset($traits['Viserio\Container\ContainerAwareTrait'])) { - $strategy->setContainer($this->container); - } - - // we must be using a custom strategy - return $strategy->dispatch($controller, $vars); - } - - /** - * Check if handler is a controller. - * - * @param string|\Closure $handler - * - * @throws \RuntimeException - * - * @return \Closure|string|array - */ - protected function isController($handler) - { - $controller = null; - - // figure out what the controller is - if (($handler instanceof Closure) || is_callable($handler)) { - $controller = $handler; - } - - if (is_string($handler) && strpos($handler, '::') !== false) { - $controller = explode('::', $handler); - } - - // if controller method wasn't specified, throw exception. - if (! $controller) { - throw new RuntimeException('A class method must be provided as a controller. ClassName::methodName'); - } - - return $controller; - } - - /** - * Handle getting of response based on strategy. - * - * @param \Viserio\Contracts\Http\Response $controller - * @param int $strategy - * @param array $vars - * - * @return ResponseContract - */ - protected function getResponseOnStrategy($controller, $strategy, $vars) - { - switch ($strategy) { - case RouteStrategyContract::URI_STRATEGY: - $response = $this->handleUriStrategy($controller, $vars); + case DispatcherContract::NOT_FOUND: + // 404 Not Found... break; - case RouteStrategyContract::RESTFUL_STRATEGY: - $response = $this->handleRestfulStrategy($controller, $vars); + case DispatcherContract::HTTP_METHOD_NOT_ALLOWED: + // 405 Method Not Allowed... break; - case RouteStrategyContract::REQUEST_RESPONSE_STRATEGY: - default: - $response = $this->handleRequestResponseStrategy($controller, $vars); + case DispatcherContract::FOUND: + // Matched route, dispatch to associated handler... break; } - - return $response; - } - - /** - * Handles response to Request -> Response Strategy based routes. - * - * @param ResponseContract $controller - * @param array $vars - * - * @return ResponseContract - */ - protected function handleRequestResponseStrategy($controller, array $vars = []) - { - $response = $this->invokeController($controller, [ - $this->container->get('request'), - $this->container->get('response'), - $vars, - ]); - - if ($response instanceof ResponseContract) { - return $response; - } - - throw new RuntimeException( - 'When using the Request -> Response Strategy your controller must return an instance of [Viserio\Contracts\Http\Response]' - ); - } - - /** - * Handles response to Restful Strategy based routes. - * - * @param ResponseContract $controller - * @param array $vars - * - * @return JsonResponse - */ - protected function handleRestfulStrategy($controller, array $vars = []) - { - try { - $response = $this->invokeController($controller, [ - $this->container['request'], - $vars, - ]); - - if ($response instanceof JsonResponse) { - return $response; - } - - if (is_array($response) || $response instanceof \ArrayObject) { - return new JsonResponse($response); - } - - throw new RuntimeException( - 'Your controller action must return a valid response for the Restful Strategy Acceptable responses are of type: [Array], [ArrayObject] and [Viserio\Http\JsonResponse]' - ); - } catch (HttpException $exception) { - $body = [ - 'status_code' => $exception->getStatusCode(), - 'message' => $exception->getMessage(), - ]; - - return new JsonResponse($body, $exception->getStatusCode(), $exception->getHeaders()); - } - } - - /** - * Handles response to URI Strategy based routes. - * - * @param ResponseContract $controller - * @param array $vars - * - * @return ResponseContract - */ - protected function handleUriStrategy($controller, array $vars) - { - $response = $this->invokeController($controller, $vars); - - if ($response instanceof ResponseContract) { - return $response; - } - - try { - $response = new Response($response); - } catch (Exception $exception) { - throw new RuntimeException('Unable to build Response from controller return value', 0, $exception); - } - - return $response; } /** - * Handle a not found route. - * - * @throws HttpException\NotFoundException - * - * @return JsonResponse + * {@inheritdoc} */ - protected function handleNotFound() + public function dispatch(string $httpMethod, string $uri): array { - $exception = new HttpException\NotFoundException(); - - if ($this->getStrategy() === RouteStrategyContract::RESTFUL_STRATEGY) { - return $exception->getJsonResponse(); - } - throw $exception; } - /** - * Handles a not allowed route. - * - * @param array $allowed - * - * @throws HttpException\MethodNotAllowedException - * - * @return JsonResponse - */ - protected function handleNotAllowed(array $allowed) + protected function generate() { - $exception = new HttpException\MethodNotAllowedException($allowed); - - if ($this->getStrategy() === RouteStrategyContract::RESTFUL_STRATEGY) { - return $exception->getJsonResponse(); - } - - throw $exception; } } diff --git a/src/Viserio/Routing/Generator/RouteTreeBuilder.php b/src/Viserio/Routing/Generator/RouteTreeBuilder.php new file mode 100644 index 000000000..b116ea967 --- /dev/null +++ b/src/Viserio/Routing/Generator/RouteTreeBuilder.php @@ -0,0 +1,8 @@ +parameterKeys; + } + + /** + * {@inheritdoc} + */ + public function getMatchedParameterExpressions(string $segmentVariable, string $uniqueKey = null): array + { + return array_fill_keys($this->parameterKeys, $segmentVariable); + } + + /** + * {@inheritdoc} + */ + public function mergeParameterKeys(SegmentMatcherContract $matcher) + { + if ($matcher->getHash() !== $this->getHash()) { + throw new RuntimeException( + sprintf( + 'Cannot merge parameters: matchers must be equivalent, \'%s\' expected, \'%s\' given.', + $matcher->getHash(), + $this->getHash() + ) + ); + } + + $this->parameterKeys = array_unique( + array_merge($this->parameterKeys, $matcher->getParameterKeys()), + SORT_NUMERIC + ); + } + + /** + * {@inheritdoc} + */ + public function getHash(): string + { + return get_class($this) . ':' . $this->getMatchHash(); + } + + /** + * Returns a unique hash for the matching criteria of the segment. + * + * @return string + */ + abstract protected function getMatchHash(): string; +} diff --git a/src/Viserio/Routing/Matchers/AnyMatcher.php b/src/Viserio/Routing/Matchers/AnyMatcher.php new file mode 100644 index 000000000..47678cec2 --- /dev/null +++ b/src/Viserio/Routing/Matchers/AnyMatcher.php @@ -0,0 +1,32 @@ +parameterKeys = $parameterKeys; + } + + /** + * {@inheritdoc} + */ + public function getConditionExpression(string $segmentVariable, string $uniqueKey = null): string + { + return $segmentVariable . ' !== \'\''; + } + + /** + * {@inheritdoc} + */ + protected function getMatchHash(): string + { + return ''; + } +} diff --git a/src/Viserio/Routing/Matchers/CompoundMatcher.php b/src/Viserio/Routing/Matchers/CompoundMatcher.php new file mode 100644 index 000000000..7aa127e87 --- /dev/null +++ b/src/Viserio/Routing/Matchers/CompoundMatcher.php @@ -0,0 +1,79 @@ +getParameterKeys()); + } + + $this->parameterKeys = $parameterKeys; + $this->matchers = $matchers; + } + + /** + * {@inheritdoc} + */ + public function getConditionExpression(string $segmentVariable, string $uniqueKey = null): string + { + $conditions = []; + + foreach ($this->matchers as $key => $matcher) { + $conditions[] = $matcher->getConditionExpression($segmentVariable, $uniqueKey . '_' . $key); + } + + return implode(' && ', $conditions); + } + + /** + * {@inheritdoc} + */ + public function getMatchedParameterExpressions(string $segmentVariable, string $uniqueKey = null): array + { + $expressions = []; + + foreach ($this->matchers as $key => $matcher) { + $matchedParameterExpressions = $matcher->getMatchedParameterExpressions( + $segmentVariable, + $uniqueKey . '_' . $key + ); + + foreach ($matchedParameterExpressions as $parameter => $expression) { + $expressions[$parameter] = $expression; + } + } + + return $expressions; + } + + /** + * {@inheritdoc} + */ + protected function getMatchHash(): string + { + $hashes = []; + + foreach ($this->matchers as $matcher) { + $hashes[] = $matcher->getHash(); + } + + return implode('::', $hashes); + } +} diff --git a/src/Viserio/Routing/Matchers/ExpressionMatcher.php b/src/Viserio/Routing/Matchers/ExpressionMatcher.php new file mode 100644 index 000000000..2788f1a4b --- /dev/null +++ b/src/Viserio/Routing/Matchers/ExpressionMatcher.php @@ -0,0 +1,51 @@ +expression = $expression; + $this->parameterKeys = $parameterKeys; + } + + /** + * Returns the used expression. + * + * @return string + */ + public function getExpression(): string + { + return $this->expression; + } + + /** + * {@inheritdoc} + */ + public function getConditionExpression(string $segmentVariable, string $uniqueKey = null): string + { + return str_replace(self::SEGMENT_PLACEHOLDER, $segmentVariable, $this->expression); + } + + /** + * {@inheritdoc} + */ + protected function getMatchHash(): string + { + return $this->expression; + } +} diff --git a/src/Viserio/Routing/Matchers/ParameterMatcher.php b/src/Viserio/Routing/Matchers/ParameterMatcher.php new file mode 100644 index 000000000..4bb4084ca --- /dev/null +++ b/src/Viserio/Routing/Matchers/ParameterMatcher.php @@ -0,0 +1,28 @@ +names = is_array($names) ? $names : [$names]; + $this->regex = $regex; + } +} diff --git a/src/Viserio/Routing/Matchers/RegexMatcher.php b/src/Viserio/Routing/Matchers/RegexMatcher.php new file mode 100644 index 000000000..a844ba169 --- /dev/null +++ b/src/Viserio/Routing/Matchers/RegexMatcher.php @@ -0,0 +1,117 @@ +regex = $regex; + + $map = [$parameterKeyGroupMap => 0]; + + $this->parameterKeyGroupMap = $map; + $this->parameterKeys = array_keys($map); + } + + /** + * Counted parameters keys. + * + * @return int + */ + public function getGroupCount(): int + { + return count(array_unique($this->parameterKeyGroupMap, SORT_NUMERIC)); + } + + /** + * Retruns the parameters key group array. + * + * @return array + */ + public function getParameterKeyGroupMap(): array + { + return $this->parameterKeyGroupMap; + } + + /** + * Retruns the used regex. + * + * @return string + */ + public function getRegex(): string + { + return $this->regex; + } + + /** + * {@inheritdoc} + */ + public function getConditionExpression(string $segmentVariable, string $uniqueKey = null): string + { + return 'preg_match(' + . VarExporter::export($this->regex) + . ', ' + . $segmentVariable + . ', ' + . '$matches' . $uniqueKey + . ')'; + } + + /** + * {@inheritdoc} + */ + public function getMatchedParameterExpressions(string $segmentVariable, string $uniqueKey = null): array + { + $matches = []; + + foreach ($this->parameterKeyGroupMap as $parameterKey => $group) { + // Use $group + 1 as the first $matches element is the full text that matched, + // we want the groups + $matches[$parameterKey] = '$matches' . $uniqueKey . '[' . ($group + 1) . ']'; + } + + return $matches; + } + + /** + * {@inheritdoc} + */ + public function mergeParameterKeys(SegmentMatcherContract $matcher) + { + parent::mergeParameterKeys($matcher); + + $this->parameterKeyGroupMap += $matcher->getParameterKeyGroupMap(); + } + + /** + * {@inheritdoc} + */ + protected function getMatchHash(): string + { + return $this->regex; + } +} diff --git a/src/Viserio/Routing/Matchers/StaticMatcher.php b/src/Viserio/Routing/Matchers/StaticMatcher.php new file mode 100644 index 000000000..0e2369ef5 --- /dev/null +++ b/src/Viserio/Routing/Matchers/StaticMatcher.php @@ -0,0 +1,64 @@ +parameterKeys = $parameterKeys ?? []; + $this->segment = $segment; + } + + /** + * {@inheritdoc} + */ + public function getConditionExpression(string $segmentVariable, string $uniqueKey = null): string + { + return $segmentVariable . ' === ' . VarExporter::export($this->segment); + } + + /** + * {@inheritdoc} + */ + public function getMatchedParameterExpressions(string $segmentVariable, string $uniqueKey = null): array + { + $keys = $this->parameterKeys; + + if (count($keys) > 0) { + return [$keys[0] => $segmentVariable]; + } + + return []; + } + + /** + * {@inheritdoc} + */ + protected function getMatchHash(): string + { + return $this->segment; + } +} diff --git a/src/Viserio/Routing/Redirect.php b/src/Viserio/Routing/Redirect.php index f6eeafa1e..c93031e0d 100644 --- a/src/Viserio/Routing/Redirect.php +++ b/src/Viserio/Routing/Redirect.php @@ -40,129 +40,19 @@ class Redirect protected $action; /** - * RouteCollection instance. + * The URL generator instance. * - * @var \Viserio\Routing\RouteCollection + * @var \Viserio\Routing\UrlGenerator */ - protected $route; - - public function __construct(RouteCollection $route) - { - $this->route = $route; - } - - /** - * [to description]. - * - * @param string $location - * - * @return $this - */ - public function to($location) - { - $this->mode = 'redirect'; - $this->location = $location; - - return $this; - } - - /** - * [toRoute description]. - * - * @param string $routeName [description] - * @param array $parameters - * - * @return $this - */ - public function toRoute($routeName, array $parameters = []) - { - $this->mode = 'named'; - $this->routeName = $routeName; - $this->parameters = $parameters; - - return $this; - } - - /** - * [toAction description]. - * - * @param [type] $action [description] - * @param array $parameters - * - * @return $this - */ - public function toAction($action, array $parameters = []) - { - $this->mode = 'action'; - $this->action = $action; - $this->parameters = $parameters; - - return $this; - } + protected $generator; /** - * [with description]. - * - * @param $key - * @param $value - * - * @internal param $ [type] $key [description] - * @internal param $ [type] $value [description] + * Create a new Redirector instance. * - * @return $this [type] [description] + * @param \Viserio\Routing\UrlGenerator */ - public function with($key, $value) + public function __construct(UrlGenerator $generator) { - //session - - return $this; - } - - /** - * [execute description]. - * - * @return bool - */ - public function execute() - { - switch ($this->mode) { - case 'redirect': - header('Location: ' . $this->location); - break; - - case 'named': - $this->route->runNamed($this->routeName, $this->parameters); - break; - - case 'action': - if (is_string($this->action)) { - $this->stringToCallback($this->action); - } - - $this->route->execute($this->action, $this->parameters); - break; - } - - return false; - } - - /** - * [stringToCallback description]. - * - * @param string $callback - * - * @throws \Exception - * - * @return bool - */ - protected function stringToCallback(&$callback) - { - if (substr_count($callback, '::') === 1) { - $callback = explode('::', $callback); - - return true; - } - - throw new \Exception('Invalid callback: ' . $callback); + $this->generator = $generator; } } diff --git a/src/Viserio/Routing/Route.php b/src/Viserio/Routing/Route.php new file mode 100644 index 000000000..8fddcb397 --- /dev/null +++ b/src/Viserio/Routing/Route.php @@ -0,0 +1,377 @@ +uri = $uri; + // According to RFC methods are defined in uppercase (See RFC 7231) + $this->httpMethods = array_map('strtoupper',(array) $methods); + $this->action = $this->parseAction($action); + + if (in_array('GET', $this->httpMethods) && ! in_array('HEAD', $this->httpMethods)) { + $this->httpMethods[] = 'HEAD'; + } + + if (isset($this->action['prefix'])) { + $this->addPrefix($this->action['prefix']); + } + } + + /** + * {@inheritdoc} + */ + public function getDomain() + { + return $this->action['domain'] ?? null; + } + + /** + * {@inheritdoc} + */ + public function getUri(): string + { + return $this->uri; + } + + /** + * {@inheritdoc} + */ + public function setUri(string $uri): RouteContract + { + $this->uri = $uri; + + return $this; + } + + /** + * {@inheritdoc} + */ + public function getName() + { + return $this->action['as'] ?? null; + } + + /** + * {@inheritdoc} + */ + public function setName(string $name): RouteContract + { + $this->action['as'] = isset($this->action['as']) ? $this->action['as'] . $name : $name; + + return $this; + } + + /** + * {@inheritdoc} + */ + public function getMethods(): array + { + return $this->httpMethods; + } + + /** + * {@inheritdoc} + */ + public function isHttpOnly(): bool + { + return in_array('http', $this->action, true); + } + + /** + * {@inheritdoc} + */ + public function isHttpsOnly(): bool + { + return in_array('https', $this->action, true); + } + + /** + * {@inheritdoc} + */ + public function getActionName(): string + { + return $this->action['controller'] ?? 'Closure'; + } + + /** + * {@inheritdoc} + */ + public function getAction(): array + { + return $this->action; + } + + /** + * {@inheritdoc} + */ + public function setAction(array $action): RouteContract + { + $this->action = $action; + + return $this; + } + + /** + * {@inheritdoc} + */ + public function addPrefix(string $prefix): RouteContract + { + $uri = rtrim($prefix, '/').'/'.ltrim($this->uri, '/'); + + $this->uri = trim($uri, '/'); + + return $this; + } + + /** + * {@inheritdoc} + */ + public function getPrefix(): string + { + return $this->action['prefix'] ?? ''; + } + + /** + * {@inheritdoc} + */ + public function setParameter($name, $value): RouteContract + { + $this->parameters[$name] = $value; + + return $this; + } + + /** + * {@inheritdoc} + */ + public function getParameter(string $name, $default = null) + { + return Arr::get($this->getParameters(), $name, $default); + } + + /** + * {@inheritdoc} + */ + public function hasParameter(string $name): bool + { + return Arr::has($this->getParameters(), $name); + } + + /** + * {@inheritdoc} + */ + public function getParameters(): array + { + if (isset($this->parameters)) { + return $this->parameters; + } + + throw new LogicException('Route is not bound.'); + } + + /** + * {@inheritdoc} + */ + public function hasParameters(): bool + { + return isset($this->parameters); + } + + /** + * {@inheritdoc} + */ + public function forgetParameter(string $name) + { + $this->getParameters(); + + unset($this->parameters[$name]); + } + + /** + * {@inheritdoc} + */ + public function isStatic(): bool + { + $this->getParameters(); + + foreach($this->parameters as $parameter) { + if ($parameter instanceof ParameterMatcher) { + return false; + } + } + + return true; + } + + /** + * {@inheritdoc} + */ + public function run() + { + $this->initInvoker(); + + return $this->invoker->call( + $this->action['uses'], + array_values($this->getParameters()) + ); + } + + /** + * {@inheritdoc} + */ + public function setRouter(RouterContract $router): RouteContract + { + $this->router = $router; + + return $this; + } + + /** + * Dynamically access route parameters. + * + * @param string $key + * + * @return mixed + */ + public function __get($key) + { + return $this->getParameter($key); + } + + /** + * Set configured invoker. + * + * @return \Viserio\Support\Invoker + */ + protected function initInvoker(): Invoker + { + if ($this->invoker === null) { + $this->invoker = (new Invoker()) + ->injectByTypeHint(true) + ->injectByParameterName(true) + ->setContainer($this->getContainer()); + } + + return $this->invoker; + } + + /** + * Parse the route action into a standard array. + * + * @param callable|array|null $action + * + * @return array + * + * @throws \UnexpectedValueException + */ + protected function parseAction($action): array + { + // If no action is passed in right away, we assume the user will make use of + // fluent routing. In that case, we set a default closure, to be executed + // if the user never explicitly sets an action to handle the given uri. + if (is_null($action)) { + return ['uses' => function () { + throw new LogicException(sprintf('Route for [%s] has no action.', $this->uri)); + }]; + } + + // If the action is already a Closure instance, we will just set that instance + // as the "uses" property. + if (is_callable($action)) { + return ['uses' => $action]; + } + + // If no "uses" property has been set, we will dig through the array to find a + // Closure instance within this list. We will set the first Closure we come across. + if (! isset($action['uses'])) { + $action['uses'] = Arr::first($action, function ($value, $key) { + return is_callable($value) && is_numeric($key); + }); + } + + if (is_string($action['uses']) && strpos($action['uses'], '::') === false) { + if (! method_exists($action, '__invoke')) { + throw new UnexpectedValueException(sprintf( + 'Invalid route action: [%s]', + $action + )); + } + + $action['uses'] = $action.'::__invoke'; + } + + return $action; + } +} diff --git a/src/Viserio/Routing/RouteCollection.php b/src/Viserio/Routing/RouteCollection.php index ea6cc42df..8c276c73d 100644 --- a/src/Viserio/Routing/RouteCollection.php +++ b/src/Viserio/Routing/RouteCollection.php @@ -2,36 +2,30 @@ declare(strict_types=1); namespace Viserio\Routing; -use Closure; -use FastRoute\DataGenerator; -use FastRoute\RouteCollector; -use FastRoute\RouteParser as FastRouteParser; -use Interop\Container\ContainerInterface as ContainerContract; -use InvalidArgumentException; -use LogicException; -use RuntimeException; -use Viserio\Contracts\Routing\RouteCollector as RouteCollectorContract; -use Viserio\Contracts\Routing\RouteStrategy as RouteStrategyContract; -use Viserio\Routing\RouteParser as ViserioRouteParser; +use Viserio\Contracts\{ + Container\Traits\ContainerAwareTrait, + Routing\Route as RouteContract +}; -class RouteCollection extends RouteCollector implements RouteStrategyContract, RouteCollectorContract +class RouteCollection { - /* - * Route strategy functionality - */ - use RouteStrategyTrait; + use ContainerAwareTrait; - /** - * @var \Interop\Container\ContainerInterface + /** + * An array of the routes keyed by method. + * + * @var array */ - protected $container; + protected $routes = []; /** + * An flattened array of all of the routes. + * * @var array */ - protected $routes = []; + protected $allRoutes = []; - /** + /** * @var array */ protected $namedRoutes = []; @@ -42,327 +36,81 @@ class RouteCollection extends RouteCollector implements RouteStrategyContract, R protected $filters = []; /** - * Constructor. - * - * @param ContainerContract $container - * @param \FastRoute\RouteParser $parser - * @param \FastRoute\DataGenerator $generator + * @var \Viserio\Routing\RouteGroup[] */ - public function __construct( - ContainerContract $container, - FastRouteParser $parser, - DataGenerator $generator - ) { - $this->container = $container; - - parent::__construct($parser, $generator); - } + protected $groups = []; /** - * Add a route to the collection. + * Add a Route instance to the collection. * - * @param string|string[] $method - * @param string $route - * @param callable $handler - * @param int $strategy + * @param \Viserio\Contracts\Routing\Route $route * - * @return \Viserio\Routing\RouteCollection + * @return \Viserio\Contracts\Routing\Route */ - public function addRoute($method, $route, $handler, $strategy = self::REQUEST_RESPONSE_STRATEGY) + public function addRoute(RouteContract $route): RouteContract { - // are we running a single strategy for the collection? - $strategy = (isset($this->strategy)) ? $this->strategy : $strategy; - - // if the handler is an anonymous function, we need to store it for later use - // by the dispatcher, otherwise we just throw the handler string at FastRoute - if ($handler instanceof Closure || (is_object($handler) && is_callable($handler))) { - $callback = $handler; - $handler = uniqid('Viserio::route::', true); - - $this->routes[$handler]['callback'] = $callback; - } elseif (is_object($handler)) { - throw new RuntimeException('Object controllers must be callable.'); - } - - $this->routes[$handler]['strategy'] = $strategy; - - $route = $this->parseRouteString($route); - - //Check for a route alias starting with @ - $matches = []; + $this->addToCollections($route); - if (preg_match(ViserioRouteParser::ALIAS_REGEX, $route, $matches)) { - $route = preg_replace(ViserioRouteParser::ALIAS_REGEX, '', $route); - $this->namedRoutes[$matches[0]] = $route; - - $handler = [ - 'name' => $matches[0], - 'handler' => $handler, - ]; - } - - parent::addRoute($method, $route, $handler); - - return $this; - } - - /** - * Builds a dispatcher based on the routes attached to this collection. - * - * @return \Viserio\Routing\Dispatcher - */ - public function getDispatcher() - { - $dispatcher = new Dispatcher($this->container, $this->routes, $this->getData()); - - if ($this->strategy !== null) { - $dispatcher->setStrategy($this->strategy); - } - - return $dispatcher; + return $route; } /** - * Map a handler to the given methods and route. - * - * @param string $route The route to match against - * @param string|callable $handler The handler for the route - * @param string|string[] $methods The HTTP methods for this handler - * @param int $strategy - */ - public function map($route, $handler, $methods = 'GET', $strategy = self::REQUEST_RESPONSE_STRATEGY) - { - $this->addRoute($methods, $route, $handler, $strategy); - } - - /** - * Add a route that responds to GET HTTP method. - * - * @param string $route - * @param string|\Closure $handler - * @param int $strategy - * - * @return \Viserio\Routing\RouteCollection - */ - public function get($route, $handler, $strategy = self::REQUEST_RESPONSE_STRATEGY) - { - return $this->addRoute('GET', $route, $handler, $strategy); - } - - /** - * Add a route that responds to POST HTTP method. - * - * @param string $route - * @param string|\Closure $handler - * @param int $strategy - * - * @return \Viserio\Routing\RouteCollection - */ - public function post($route, $handler, $strategy = self::REQUEST_RESPONSE_STRATEGY) - { - return $this->addRoute('POST', $route, $handler, $strategy); - } - - /** - * Add a route that responds to PUT HTTP method. - * - * @param string $route - * @param string|\Closure $handler - * @param int $strategy - * - * @return \Viserio\Routing\RouteCollection - */ - public function put($route, $handler, $strategy = self::REQUEST_RESPONSE_STRATEGY) - { - return $this->addRoute('PUT', $route, $handler, $strategy); - } - - /** - * Add a route that responds to PATCH HTTP method. - * - * @param string $route - * @param string|\Closure $handler - * @param int $strategy - * - * @return \Viserio\Routing\RouteCollection - */ - public function patch($route, $handler, $strategy = self::REQUEST_RESPONSE_STRATEGY) - { - return $this->addRoute('PATCH', $route, $handler, $strategy); - } - - /** - * Add a route that responds to DELETE HTTP method. - * - * @param string $route - * @param string|\Closure $handler - * @param int $strategy + * Get all of the routes in the collection. * - * @return \Viserio\Routing\RouteCollection - */ - public function delete($route, $handler, $strategy = self::REQUEST_RESPONSE_STRATEGY) - { - return $this->addRoute('DELETE', $route, $handler, $strategy); - } - - /** - * Add a route that responds to HEAD HTTP method. - * - * @param string $route - * @param string|\Closure $handler - * @param int $strategy - * - * @return \Viserio\Routing\RouteCollection - */ - public function head($route, $handler, $strategy = self::REQUEST_RESPONSE_STRATEGY) - { - return $this->addRoute('HEAD', $route, $handler, $strategy); - } - - /** - * Add a route that responds to OPTIONS HTTP method. - * - * @param string $route - * @param string|\Closure $handler - * @param int $strategy - * - * @return \Viserio\Routing\RouteCollection - */ - public function options($route, $handler, $strategy = self::REQUEST_RESPONSE_STRATEGY) - { - return $this->addRoute('OPTIONS', $route, $handler, $strategy); - } - - /** - * Add a route that responds to ANY HTTP method. - * - * @param string $route - * @param string|\Closure $handler - * @param int $strategy - * - * @return \Viserio\Routing\RouteCollection - */ - public function any($route, $handler, $strategy = self::REQUEST_RESPONSE_STRATEGY) - { - return $this->addRoute('ANY', $route, $handler, $strategy); - } - - /** - * Add a "before" event listener. - * - * @param string $name - * @param callable $handler - * @param int $priority + * @return array */ - public function onBefore($name, $handler, $priority = 0) + public function getRoutes(): array { - $this->addEventListener($name, $handler, 'before', $priority); + return array_values($this->allRoutes); } /** - * Add an "after" event listener. + * Add the given route to the arrays of routes. * - * @param string $name - * @param callable $handler - * @param int $priority + * @param \Viserio\Contracts\Routing\Route $route */ - public function onAfter($name, $handler, $priority = 0) + protected function addToCollections(RouteContract $route) { - $this->addEventListener($name, $handler, 'after', $priority); - } + $domainAndUri = $route->getDomain() . $route->getUri(); - /** - * Add a global "before" event listener. - * - * @param callable $handler - * @param int $priority - */ - public function globalOnBefore($handler, $priority = 0) - { - $this->addEventListener(null, $handler, 'before', $priority); - } + foreach ($route->getMethods() as $method) { + $this->routes[$method][$domainAndUri] = $route; + } - /** - * Add a global "after" event listener. - * - * @param callable $handler - * @param int $priority - */ - public function globalOnAfter($handler, $priority = 0) - { - $this->addEventListener(null, $handler, 'after', $priority); + $this->allRoutes[$method.$domainAndUri] = $route; } /** - * Redirect instance. * - * @return \Viserio\Routing\Redirect - */ - public function redirect() - { - return new Redirect($this); - } - - /** - * Returns the array of registered named routes (starting with @). + * @param string $pattern [description] * * @return array */ - public function getNamedRoutes() + protected function parseRoutingPattern(string $pattern): array { - return $this->namedRoutes; - } + if (is_string($pattern)) { + return [$pattern, []]; + } - /** - * @param string|null $name - * @param callable $handler - * @param string $when - * @param int $priority - */ - protected function addEventListener($name, $handler, $when, $priority) - { - if ($name) { - if (array_key_exists($name, $this->filters)) { - throw new LogicException(sprintf('Filter with name %s already defined', $name)); + if (is_array($pattern)) { + if (!isset($pattern[0]) || !is_string($pattern[0])) { + throw new InvalidRoutePatternException(sprintf( + 'Cannot add route: route pattern array must have the first element containing the pattern string, %s given', + isset($pattern[0]) ? gettype($pattern[0]) : 'none' + )); } - $this->filters[$name] = $name; - } - - $name = $name ? sprintf('route%s%s', $when, $name) : sprintf('route%s', $when); + $patternString = $pattern[0]; + $parameterConditions = $pattern; - $this->container['events']->addListener($name, $handler, $priority); - } + unset($parameterConditions[0]); - /** - * Get filter. - * - * @param string $name - */ - protected function getFilter($name) - { - if (! array_key_exists($name, $this->filters)) { - throw new InvalidArgumentException(sprintf('Filter with name %s is not defined', $name)); + return [$patternString, $parameterConditions]; } - return $this->filters[$name]; - } - - /** - * Convenience method to convert pre-defined key words in to regex strings. - * - * @param string $route - * - * @return string - */ - protected function parseRouteString($route) - { - $wildcards = [ - '/{(.+?):number}/' => '{$1:[0-9]+}', - '/{(.+?):word}/' => '{$1:[a-zA-Z]+}', - '/{(.+?):alphanum_dash}/' => '{$1:[a-zA-Z0-9-_]+}', - ]; - - return preg_replace(array_keys($wildcards), array_values($wildcards), $route); + throw new InvalidRoutePatternException(sprintf( + 'Cannot add route: route pattern must be a pattern string, %s given', + gettype($pattern) + )); } } diff --git a/src/Viserio/Routing/RouteGroup.php b/src/Viserio/Routing/RouteGroup.php new file mode 100644 index 000000000..3f1390e34 --- /dev/null +++ b/src/Viserio/Routing/RouteGroup.php @@ -0,0 +1,67 @@ +callback = $callback; + $this->collection = $collection; + $this->prefix = sprintf('/%s', ltrim($prefix, '/')); + } + + /** + * Process the group and ensure routes are added to the collection. + */ + public function __invoke() + { + call_user_func_array($this->callback, [$this]); + } + + /** + * {@inheritdoc} + */ + public function map($method, $path, $handler) + { + $path = ($path === '/') ? $this->prefix : $this->prefix . sprintf('/%s', ltrim($path, '/')); + $route = $this->collection->map($method, $path, $handler); + $route->setParentGroup($this); + + if ($host = $this->getHost()) { + $route->setHost($host); + } + + if ($scheme = $this->getScheme()) { + $route->setScheme($scheme); + } + + foreach ($this->getMiddlewareStack() as $middleware) { + $route->middleware($middleware); + } + + return $route; + } +} diff --git a/src/Viserio/Routing/RouteParser.php b/src/Viserio/Routing/RouteParser.php index 3fcae90ce..0d0cca471 100644 --- a/src/Viserio/Routing/RouteParser.php +++ b/src/Viserio/Routing/RouteParser.php @@ -2,30 +2,153 @@ declare(strict_types=1); namespace Viserio\Routing; -use FastRoute\RouteParser as FastRouteParser; -use FastRoute\RouteParser\Std; +use Viserio\Routing\Matchers\{ + StaticMatcher, + ParameterMatcher +}; +use Viserio\Contracts\Routing\{ + Exceptions\InvalidRoutePatternException, + RouteParser as RouteParserContract, + RouteSegment as RouteSegmentContract, + Pattern +}; -class RouteParser extends Std implements FastRouteParser +class RouteParser implements RouteParserContract { /** - * Regex to find the route alias. + * {@inheritdoc} */ - const ALIAS_REGEX = '/^(@[a-zA-Z0-9-_\.]+)/'; + public function parse(string $route, array $conditions): array + { + if (strlen($route) > 1 && $route[0] !== '/') { + throw new InvalidRoutePatternException(sprintf( + 'Invalid route pattern: non-root route must be prefixed with \'/\', \'%s\' given', + $route + )); + } + + $segments = []; + $matches = []; + $names = []; + $patternSegments = explode('/', $route); + + array_shift($patternSegments); + + foreach ($patternSegments as $key => $patternSegment) { + if ($this->matchRouteParameters($route, $patternSegment, $conditions, $matches, $names)) { + $segments[] = new ParameterMatcher( + $names, + $this->generateRegex($matches, $conditions) + ); + } else { + $segments[] = new StaticMatcher($patternSegment); + } + } + + return $segments; + } /** - * Parses the string into an array of segments. - * - * "/user/{name}/{id:[0-9]+}" + * Validate and match uri paramters. * * @param string $route + * @param string $patternSegment + * @param array &$conditions + * @param array &$matches + * @param array &$names * - * @return array + * @return bool */ - public function parse($route) + protected function matchRouteParameters( + string $route, + string $patternSegment, + array &$conditions, + array &$matches, + array &$names + ): bool { + $matchedParameter = false; + $names = []; + $matches = []; + $current = ''; + $inParameter = false; + + foreach (str_split($patternSegment) as $character) { + if ($inParameter) { + if ($character === '}') { + if (strpos($current, ':') !== false) { + $regex = substr($current, strpos($current, ':') + 1); + $current = substr($current, 0, strpos($current, ':')); + $conditions[$current] = $regex; + } + + $matches[] = [self::PARAMETER_PART, $current]; + $names[] = $current; + $current = ''; + $inParameter = false; + $matchedParameter = true; + + continue; + } elseif ($character === '{') { + throw new InvalidRoutePatternException(sprintf( + 'Invalid route uri: cannot contain nested \'{\', \'%s\' given', + $route + )); + } + } else { + if ($character === '{') { + $matches[] = [self::STATIC_PART, $current]; + $current = ''; + $inParameter = true; + + continue; + } elseif ($character === '}') { + throw new InvalidRoutePatternException(sprintf( + 'Invalid route uri: cannot contain \'}\' before opening \'{\', \'%s\' given', + $route + )); + } + } + + $current .= $character; + } + + if ($inParameter) { + throw new InvalidRoutePatternException(sprintf( + 'Invalid route uri: cannot contain \'{\' without closing \'}\', \'%s\' given', + $route + )); + } elseif ($current !== '') { + $matches[] = [self::STATIC_PART, $current]; + } + + return $matchedParameter; + } + + /** + * Generate a segment regex. + * + * @param array $matches + * @param array $parameterPatterns + * + * @return string + */ + protected function generateRegex(array $matches, array $parameterPatterns): string { - //Remove possible name in route - $route = preg_replace(self::ALIAS_REGEX, '', $route); + $regex = '/^'; + + foreach ($matches as $match) { + list($type, $part) = $match; + + if ($type === self::STATIC_PART) { + $regex .= preg_quote($part, '/'); + } else { + // Parameter, $part is the parameter name + $regex .= '(' . ($parameterPatterns[$part] ?? Pattern::ANY) . ')'; + } + } + + $regex .= '$/'; - return parent::parse($route); + return $regex; } } diff --git a/src/Viserio/Routing/RouteStrategyTrait.php b/src/Viserio/Routing/RouteStrategyTrait.php deleted file mode 100644 index 6b39d8fcc..000000000 --- a/src/Viserio/Routing/RouteStrategyTrait.php +++ /dev/null @@ -1,43 +0,0 @@ -strategy = $strategy; - - return; - } - - throw new InvalidArgumentException( - 'Provided strategy must be an integer or an instance of [\Viserio\Contracts\Routing\CustomStrategy]' - ); - } - - /** - * Gets global strategy. - * - * @return int - */ - public function getStrategy() - { - return $this->strategy; - } -} diff --git a/src/Viserio/Routing/Router.php b/src/Viserio/Routing/Router.php new file mode 100644 index 000000000..d1b8ba822 --- /dev/null +++ b/src/Viserio/Routing/Router.php @@ -0,0 +1,203 @@ +container = $container; + $this->parser = $parser; + } + + /** + * {@inheritdoc} + */ + public function get(string $uri, $action = null): RouteContract + { + return $this->addRoute(['GET', 'HEAD'], $uri, $action); + } + + /** + * {@inheritdoc} + */ + public function post(string $uri, $action = null): RouteContract + { + return $this->addRoute('POST', $uri, $action); + } + + /** + * {@inheritdoc} + */ + public function put(string $uri, $action = null): RouteContract + { + return $this->addRoute('PUT', $uri, $action); + } + + /** + * {@inheritdoc} + */ + public function patch(string $uri, $action = null): RouteContract + { + return $this->addRoute('PATCH', $uri, $action); + } + + /** + * {@inheritdoc} + */ + public function delete(string $uri, $action = null): RouteContract + { + return $this->addRoute('DELETE', $uri, $action); + } + + /** + * {@inheritdoc} + */ + public function options(string $uri, $action = null): RouteContract + { + return $this->addRoute('OPTIONS', $uri, $action); + } + + /** + * {@inheritdoc} + */ + public function any(string $uri, $action = null): RouteContract + { + $verbs = ['GET', 'HEAD', 'POST', 'PUT', 'PATCH', 'DELETE']; + + return $this->addRoute($verbs, $uri, $action); + } + + /** + * {@inheritdoc} + */ + public function match($methods, $uri, $action = null): RouteContract + { + return $this->addRoute(array_map('strtoupper', (array) $methods), $uri, $action); + } + + /** + * {@inheritdoc} + */ + public function getGroup(): RouteGroupContract + { + return $this->group; + } + + /** + * {@inheritdoc} + */ + public function group(array $attributes, Closure $callback): RouterContract + { + $this->group = $group; + + return $this; + } + + /** + * Dispatch router for HTTP request. + * + * @param \Psr\Http\Message\ServerRequestInterface $request The current HTTP request object + * + * @return array + */ + public function dispatch(ServerRequestInterface $request) + { + + } + + /** + * Add a route to the underlying route collection. + * + * @param array|string $methods + * @param string $uri + * @param \Closure|array|string|null $action + * + * @return \Viserio\Contracts\Routing\Route + */ + protected function addRoute($methods, string $uri, $action): RouteContract + { + return $this->routes[] = $this->createRoute($methods, $uri, $action); + } + + /** + * Create a new route instance. + * + * @param array|string $methods + * @param string $uri + * @param mixed $action + * + * @return \Viserio\Contracts\Routing\Route + */ + protected function createRoute($methods, string $uri, $action): RouteContract + { + $pattern = $this->parser->parse($uri, ['TODO']); + + $route = $this->newRoute( + $methods, + $this->prefix($uri), + $action + ); + + foreach ($pattern as $key => $value) { + $route->setParameter($key, $value); + } + + return $route; + } + + /** + * Create a new Route object. + * + * @param array|string $methods + * @param string $uri + * @param mixed $action + * + * @return \Viserio\Contracts\Routing\Route + */ + protected function newRoute($methods, string $uri, $action): Route + { + return (new Route($methods, $uri, $action)) + ->setRouter($this) + ->setContainer($this->container); + } + + /** + * Prefix the given URI with the last prefix. + * + * @param string $uri + * @return string + */ + protected function prefix($uri) + { + return trim('/'.trim($uri, '/'), '/') ?: '/'; + } +} diff --git a/src/Viserio/Routing/Tests/DispatcherTest.php b/src/Viserio/Routing/Tests/DispatcherTest.php index 0019dcf62..3c1df5cd5 100644 --- a/src/Viserio/Routing/Tests/DispatcherTest.php +++ b/src/Viserio/Routing/Tests/DispatcherTest.php @@ -2,19 +2,21 @@ declare(strict_types=1); namespace Viserio\Routing\Tests; -use FastRoute\DataGenerator\GroupCountBased; -use Viserio\Container\Container; -use Viserio\Routing\RouteCollection; -use Viserio\Routing\RouteParser; +use Viserio\Routing\{ + Dispatcher, + Route, + RouteCollection +}; class DispatcherTest extends \PHPUnit_Framework_TestCase { - private function getRouteCollection() + public function testMatch() { - return new RouteCollection( - new Container(), - new RouteParser(), - new GroupCountBased() - ); + $route = new Route('GET', 'test', null); + + $collection = new RouteCollection(); + $collection->addRoute($route); + + $dispatcher = new Dispatcher($collection); } } diff --git a/src/Viserio/Routing/Tests/Fixture/Controller.php b/src/Viserio/Routing/Tests/Fixture/Controller.php new file mode 100644 index 000000000..e7087feb5 --- /dev/null +++ b/src/Viserio/Routing/Tests/Fixture/Controller.php @@ -0,0 +1,13 @@ +assertSame('segment/[test] !== \'\'', $matcher->getConditionExpression('segment/[test]')); + } + + public function testAnyMergingParameterKeys() + { + $matcher1 = new AnyMatcher([123]); + $matcher2 = new AnyMatcher([12, 3]); + $matcher1->mergeParameterKeys($matcher2); + + $this->assertSame([123, 12, 3], $matcher1->getParameterKeys()); + } +} diff --git a/src/Viserio/Routing/Tests/Matchers/CompoundMatcherTest.php b/src/Viserio/Routing/Tests/Matchers/CompoundMatcherTest.php new file mode 100644 index 000000000..e2df5f019 --- /dev/null +++ b/src/Viserio/Routing/Tests/Matchers/CompoundMatcherTest.php @@ -0,0 +1,55 @@ +assertSame('test === \'test\' && test !== \'\'', $matcher->getConditionExpression('test', '2')); + } + + public function testGetMatchedParameterExpressions() + { + $matcher = new CompoundMatcher([ + new StaticMatcher('test', [1]), + new AnyMatcher([0]) + ]); + + $this->assertSame([1 => 'test', 0 => 'test'], $matcher->getMatchedParameterExpressions('test', '2')); + } + + public function testGetHash() + { + $matcher = new CompoundMatcher([ + new StaticMatcher('test', [1]), + new AnyMatcher([0]) + ]); + + $this->assertSame('Viserio\Routing\Matchers\CompoundMatcher:Viserio\Routing\Matchers\StaticMatcher:test::Viserio\Routing\Matchers\AnyMatcher:', $matcher->getHash()); + } + + public function testCompoundSegmentMatcher() + { + $matcher1 = new CompoundMatcher([new StaticMatcher('a'), new StaticMatcher('b', [0])]); + $matcher2 = new CompoundMatcher([new StaticMatcher('a', [0]), new StaticMatcher('c', [1])]); + + $this->assertSame([0], $matcher1->getParameterKeys()); + $this->assertNotEquals($matcher2->getHash(), $matcher1->getHash()); + $this->assertSame('$segment === \'a\' && $segment === \'b\'', $matcher1->getConditionExpression('$segment', '0')); + $this->assertSame([0 => '$segment'], $matcher1->getMatchedParameterExpressions('$segment', '0')); + $this->assertSame('$segment === \'a\' && $segment === \'c\'', $matcher2->getConditionExpression('$segment', '0')); + $this->assertSame([0 => '$segment', 1 => '$segment'], $matcher2->getMatchedParameterExpressions('$segment', '0')); + } +} diff --git a/src/Viserio/Routing/Tests/Matchers/ExpressionMatcherTest.php b/src/Viserio/Routing/Tests/Matchers/ExpressionMatcherTest.php new file mode 100644 index 000000000..26eac0b74 --- /dev/null +++ b/src/Viserio/Routing/Tests/Matchers/ExpressionMatcherTest.php @@ -0,0 +1,36 @@ +assertSame('ctype_digit({segment})', $matcher->getExpression()); + } + + public function testGetConditionExpression() + { + $matcher = new ExpressionMatcher('ctype_digit({segment})', [1]); + + $this->assertSame('ctype_digit(test)', $matcher->getConditionExpression('test')); + } + + /** + * @expectedException RuntimeException + * @expectedExceptionMessage Cannot merge parameters: matchers must be equivalent, 'Viserio\Routing\Matchers\StaticMatcher:two' expected, 'Viserio\Routing\Matchers\ExpressionMatcher:ctype_digit({segment})' given. + */ + public function testMergeParameterKeys() + { + $matcher = new ExpressionMatcher('ctype_digit({segment})', [1]); + $matcher2 = new StaticMatcher('two', [3]); + $matcher->mergeParameterKeys($matcher2); + } +} diff --git a/src/Viserio/Routing/Tests/Matchers/RegexMatcherTest.php b/src/Viserio/Routing/Tests/Matchers/RegexMatcherTest.php new file mode 100644 index 000000000..e9f953c10 --- /dev/null +++ b/src/Viserio/Routing/Tests/Matchers/RegexMatcherTest.php @@ -0,0 +1,54 @@ +assertSame(1, $matcher->getGroupCount()); + } + + public function testGetRegex() + { + $matcher = new RegexMatcher('/^(' . Pattern::ALPHA . ')$/', 12); + + $this->assertSame('/^(' . Pattern::ALPHA . ')$/', $matcher->getRegex()); + } + + public function testGetParameterKeyGroupMap() + { + $matcher = new RegexMatcher('/^(' . Pattern::ALPHA . ')$/', 12); + + $this->assertSame([12 => 0], $matcher->getParameterKeyGroupMap()); + } + + public function testGetConditionExpression() + { + $matcher = new RegexMatcher('/^(' . Pattern::ALPHA . ')$/', 12); + + $this->assertSame('preg_match(\'/^([a-zA-Z]+)$/\', test, $matches)', $matcher->getConditionExpression('test')); + } + + public function testGetMatchedParameterExpressions() + { + $matcher = new RegexMatcher('/^(' . Pattern::ALPHA . ')$/', 12); + + $this->assertSame([12 => '$matches[1]'], $matcher->getMatchedParameterExpressions('test')); + } + + public function testRegexMergingParameterKeys() + { + $matcher1 = new RegexMatcher('/^(' . Pattern::ANY . ')$/', 12); + $matcher2 = new RegexMatcher('/^(' . Pattern::ANY . ')$/', 11); + $matcher1->mergeParameterKeys($matcher2); + + $this->assertSame([12, 11], $matcher1->getParameterKeys()); + $this->assertSame([12 => 0, 11 => 0], $matcher1->getParameterKeyGroupMap()); + } +} diff --git a/src/Viserio/Routing/Tests/Matchers/StaticMatcherTest.php b/src/Viserio/Routing/Tests/Matchers/StaticMatcherTest.php new file mode 100644 index 000000000..80f880c9f --- /dev/null +++ b/src/Viserio/Routing/Tests/Matchers/StaticMatcherTest.php @@ -0,0 +1,43 @@ +assertSame('one === \'one\'', $matcher->getConditionExpression('one')); + } + + public function testGetMatchedParameterExpressions() + { + $matcher = new StaticMatcher('two', [1]); + + $this->assertSame([1 => 'two'], $matcher->getMatchedParameterExpressions('two')); + + $matcher = new StaticMatcher('three'); + + $this->assertSame([], $matcher->getMatchedParameterExpressions('three')); + } + + public function testMergeParameterKeys() + { + $matcher = new StaticMatcher('two', [2]); + $matcher2 = new StaticMatcher('two', [3]); + $matcher->mergeParameterKeys($matcher2); + + $this->assertSame([2 => 'two'], $matcher->getMatchedParameterExpressions('two')); + } +} diff --git a/src/Viserio/Routing/Tests/RouteCollectionTest.php b/src/Viserio/Routing/Tests/RouteCollectionTest.php index c9a5679dc..53739f283 100644 --- a/src/Viserio/Routing/Tests/RouteCollectionTest.php +++ b/src/Viserio/Routing/Tests/RouteCollectionTest.php @@ -2,214 +2,10 @@ declare(strict_types=1); namespace Viserio\Routing\Tests; -use FastRoute\DataGenerator\GroupCountBased; -use Viserio\Container\Container; use Viserio\Routing\RouteCollection; use Viserio\Routing\RouteParser; class RouteCollectionTest extends \PHPUnit_Framework_TestCase { - /** - * Asserts that routes are set via convenience methods. - */ - public function testSetsRoutesViaConvenienceMethods() - { - $router = $this->getRouteCollection(); - $router->get('/route/{wildcard}', 'handler_get', RouteCollection::RESTFUL_STRATEGY); - $router->post('/route/{wildcard}', 'handler_post', RouteCollection::URI_STRATEGY); - $router->put('/route/{wildcard}', 'handler_put', RouteCollection::REQUEST_RESPONSE_STRATEGY); - $router->patch('/route/{wildcard}', 'handler_patch'); - $router->delete('/route/{wildcard}', 'handler_delete'); - $router->head('/route/{wildcard}', 'handler_head'); - $router->options('/route/{wildcard}', 'handler_options'); - - $routes = (new \ReflectionClass($router))->getProperty('routes'); - $routes->setAccessible(true); - $routes = $routes->getValue($router); - - $this->assertCount(7, $routes); - - $this->assertSame($routes['handler_get'], ['strategy' => 1]); - $this->assertSame($routes['handler_post'], ['strategy' => 2]); - $this->assertSame($routes['handler_put'], ['strategy' => 0]); - $this->assertSame($routes['handler_patch'], ['strategy' => 0]); - $this->assertSame($routes['handler_delete'], ['strategy' => 0]); - $this->assertSame($routes['handler_head'], ['strategy' => 0]); - $this->assertSame($routes['handler_options'], ['strategy' => 0]); - } - - /** - * Asserts that routes are set via convenience methods with Closures. - */ - public function testSetsRoutesViaConvenienceMethodsWithClosures() - { - $router = $this->getRouteCollection(); - - $router->get('/route/{wildcard}', function () { - return 'get'; - }); - $router->post('/route/{wildcard}', function () { - return 'post'; - }); - $router->put('/route/{wildcard}', function () { - return 'put'; - }); - $router->patch('/route/{wildcard}', function () { - return 'patch'; - }); - $router->delete('/route/{wildcard}', function () { - return 'delete'; - }); - $router->head('/route/{wildcard}', function () { - return 'head'; - }); - $router->options('/route/{wildcard}', function () { - return 'options'; - }); - $router->any('/route/{wildcard}', function () { - return 'any'; - }); - - $routes = (new \ReflectionClass($router))->getProperty('routes'); - $routes->setAccessible(true); - $routes = $routes->getValue($router); - - $this->assertCount(8, $routes); - - foreach ($routes as $route) { - $this->assertArrayHasKey('callback', $route); - $this->assertArrayHasKey('strategy', $route); - } - } - - /** - * Asserts that global strategy is used when set. - */ - public function testGlobalStrategyIsUsedWhenSet() - { - $router = $this->getRouteCollection(); - - $router->setStrategy(RouteCollection::URI_STRATEGY); - $router->get('/route/{wildcard}', 'handler_get', RouteCollection::RESTFUL_STRATEGY); - $router->post('/route/{wildcard}', 'handler_post', RouteCollection::URI_STRATEGY); - $router->put('/route/{wildcard}', 'handler_put', RouteCollection::REQUEST_RESPONSE_STRATEGY); - $router->patch('/route/{wildcard}', 'handler_patch'); - $router->delete('/route/{wildcard}', 'handler_delete'); - $router->head('/route/{wildcard}', 'handler_head'); - $router->options('/route/{wildcard}', 'handler_options'); - $routes = (new \ReflectionClass($router))->getProperty('routes'); - $routes->setAccessible(true); - $routes = $routes->getValue($router); - - $this->assertCount(7, $routes); - - $this->assertSame($routes['handler_get'], ['strategy' => 2]); - $this->assertSame($routes['handler_post'], ['strategy' => 2]); - $this->assertSame($routes['handler_put'], ['strategy' => 2]); - $this->assertSame($routes['handler_patch'], ['strategy' => 2]); - $this->assertSame($routes['handler_delete'], ['strategy' => 2]); - $this->assertSame($routes['handler_head'], ['strategy' => 2]); - $this->assertSame($routes['handler_options'], ['strategy' => 2]); - } - - /** - * Asserts that an exception is thrown when an incorrect strategy type is provided. - */ - public function testExceptionIsThrownWhenWrongStrategyTypeProvided() - { - $this->setExpectedException('InvalidArgumentException'); - $router = $this->getRouteCollection(); - $router->setStrategy('hello'); - } - - /** - * Asserts that `getDispatcher` method returns correct instance. - */ - public function testCollectionReturnsDispatcher() - { - $router = $this->getRouteCollection(); - $this->assertInstanceOf('Viserio\Routing\Dispatcher', $router->getDispatcher()); - $this->assertInstanceOf('FastRoute\Dispatcher\GroupCountBased', $router->getDispatcher()); - } - - /** - * Asserts named routes are put in namedRoute array. - */ - public function testNamedRoutesAreProperlyHandled() - { - $router = $this->getRouteCollection(); - - $router->addRoute('GET', 'noname', function () { - }); - $router->addRoute('GET', '@name/named-route', function () { - }); - $router->addRoute('GET', '@another-name/another-named-route', function () { - }); - - $data = $router->getData(); - - $this->assertCount(2, $router->getNamedRoutes()); - $this->assertCount(3, $data); - $this->assertEquals(['GET', '/', ['handler' => 'handler0', 'name' => 'name']], $data[1]); - $this->assertEquals(['GET', '/', ['handler' => 'handler0', 'name' => 'another-name']], $data[2]); - } - - public function testCallableControllers() - { - $router = $this->getRouteCollection(); - - $router->get('/', new CallableController()); - - $routes = (new \ReflectionClass($router))->getProperty('routes'); - $routes->setAccessible(true); - $routes = $routes->getValue($router); - - $this->assertCount(1, $routes); - } - - /** - * @expectedException \RuntimeException - */ - public function testNonCallbleObjectControllersError() - { - $router = $this->getRouteCollection(); - - $router->get('/', new \stdClass()); - - $routes = (new \ReflectionClass($router))->getProperty('routes'); - $routes->setAccessible(true); - $routes = $routes->getValue($router); - - $this->assertCount(0, $routes); - } - - public function testRedirect() - { - $router = $this->getRouteCollection(); - - $this->assertSame($router->redirect(), new \Viserio\Routing\Redirect($router)); - } - - private function getRouteCollection() - { - return new RouteCollection( - new Container(), - new RouteParser(), - new GroupCountBased() - ); - } -} - -class CallableController -{ - /** - * @param Symfony\Component\HttpFoundation\Request $request - * @param Symfony\Component\HttpFoundation\Response $response - */ - public function __invoke( - Symfony\Component\HttpFoundation\Request $request, - Symfony\Component\HttpFoundation\Response $response - ) { - } } diff --git a/src/Viserio/Routing/Tests/RouteParserTest.php b/src/Viserio/Routing/Tests/RouteParserTest.php index 3c0cffc50..b45b409d3 100644 --- a/src/Viserio/Routing/Tests/RouteParserTest.php +++ b/src/Viserio/Routing/Tests/RouteParserTest.php @@ -2,17 +2,173 @@ declare(strict_types=1); namespace Viserio\Routing\Tests; -use Viserio\Routing\RouteParser; +use RuntimeException; +use Viserio\Routing\{ + RouteParser, + Matchers\StaticMatcher, + Matchers\ParameterMatcher +}; +use Viserio\Contracts\Routing\{ + Exceptions\InvalidRoutePatternException, + Pattern +}; class RouteParserTest extends \PHPUnit_Framework_TestCase { /** - * Asserts that the @ sign is correctly added when missing. + * @dataProvider routeParsingProvider */ - public function testNamedRouteAreHandledTheSameAsNotNamedRoute() + public function testRouteParser($pattern, array $conditions, array $expectedSegments) { $parser = new RouteParser(); - $this->assertEquals($parser->parse('@bundle.named_route/my-route'), $parser->parse('/my-route')); + $this->assertEquals($expectedSegments, $parser->parse($pattern, $conditions)); + } + + public function routeParsingProvider() + { + return [ + [ + // Empty route + '', + [], + [] + ], + [ + // Empty route + '/', + [], + [new StaticMatcher('')] + ], + [ + '/user', + [], + [new StaticMatcher('user')] + ], + [ + '/user/', + [], + [new StaticMatcher('user'), new StaticMatcher('')] + ], + [ + '/user/profile', + [], + [new StaticMatcher('user'), new StaticMatcher('profile')] + ], + [ + '/{parameter}', + [], + [new ParameterMatcher('parameter', '/^(' . Pattern::ANY. ')$/')] + ], + [ + '/{param}', + ['param' => Pattern::ALPHA_NUM], + [new ParameterMatcher('param', '/^(' . Pattern::ALPHA_NUM . ')$/')] + ], + [ + '/user/{id}/profile/{type}', + ['id' => Pattern::DIGITS, 'type' => Pattern::ALPHA_LOWER], + [ + new StaticMatcher('user'), + new ParameterMatcher('id', '/^(' . Pattern::DIGITS . ')$/'), + new StaticMatcher('profile'), + new ParameterMatcher('type', '/^(' . Pattern::ALPHA_LOWER . ')$/'), + ] + ], + [ + '/prefix{param}', + ['param' => Pattern::ALPHA_NUM], + [new ParameterMatcher(['param'], '/^prefix(' . Pattern::ALPHA_NUM . ')$/')] + ], + [ + '/{param}suffix', + ['param' => Pattern::ALPHA_NUM], + [new ParameterMatcher(['param'], '/^(' . Pattern::ALPHA_NUM . ')suffix$/')] + ], + [ + '/abc{param1}:{param2}', + ['param1' => Pattern::ANY, 'param2' => Pattern::ALPHA], + [new ParameterMatcher(['param1', 'param2'], '/^abc(' . Pattern::ANY . ')\:(' . Pattern::ALPHA . ')$/')] + ], + [ + '/shop/{category}:{product}/buy/quantity:{quantity}', + ['category' => Pattern::ALPHA, 'product' => Pattern::ALPHA, 'quantity' => Pattern::DIGITS], + [ + new StaticMatcher('shop'), + new ParameterMatcher(['category', 'product'], '/^(' . Pattern::ALPHA . ')\:(' . Pattern::ALPHA . ')$/'), + new StaticMatcher('buy'), + new ParameterMatcher(['quantity'], '/^quantity\:(' . Pattern::DIGITS . ')$/'), + ] + ], + [ + '/{param:[0-9]+}', + [], + [new ParameterMatcher(['param'], '/^([0-9]+)$/'),] + ], + [ + '/{param:[\:]+}', + [], + [new ParameterMatcher(['param'], '/^([\:]+)$/'),] + ], + [ + // Inline regexps take precedence + '/{param:[a-z]+}', + ['param' => Pattern::ALPHA_UPPER], + [new ParameterMatcher(['param'], '/^([a-z]+)$/'),] + ], + [ + '/abc{param1:.+}:{param2:.+}', + [], + [new ParameterMatcher(['param1', 'param2'], '/^abc(.+)\:(.+)$/')] + ], + [ + '/shop/{category:[\w]+}:{product:[\w]+}/buy/quantity:{quantity:[0-9]+}', + [], + [ + new StaticMatcher('shop'), + new ParameterMatcher(['category', 'product'], '/^([\w]+)\:([\w]+)$/'), + new StaticMatcher('buy'), + new ParameterMatcher(['quantity'], '/^quantity\:([0-9]+)$/'), + ] + ], + ]; + } + + /** + * @dataProvider invalidParsingProvider + */ + public function testInvalidRouteParsing($uri, $expectedExceptionType) { + $this->setExpectedExceptionRegExp( + $expectedExceptionType ?: RuntimeException::class, + '/.*/' + ); + + (new RouteParser())->parse($uri, []); + } + + public function invalidParsingProvider() + { + return [ + [ + 'abc', + InvalidRoutePatternException::class, + ], + [ + '/test/{a/bc}', + InvalidRoutePatternException::class, + ], + [ + '/test/{a{bc}', + InvalidRoutePatternException::class, + ], + [ + '/test/{abc}}', + InvalidRoutePatternException::class, + ], + [ + '/test/{a{bc}}', + InvalidRoutePatternException::class, + ], + ]; } } diff --git a/src/Viserio/Routing/Tests/RouteTest.php b/src/Viserio/Routing/Tests/RouteTest.php new file mode 100644 index 000000000..3206ae1b5 --- /dev/null +++ b/src/Viserio/Routing/Tests/RouteTest.php @@ -0,0 +1,110 @@ +getRouter(); + // $router->get('/hello/{name}', function (Request $request, Response $response) { + // $name = $request->getAttribute('name'); + // $response->getBody()->write("Hello, $name"); + + // return $response; + // }); + } + + public function testGetMethods() + { + $route = new Route('GET', 'test', ['uses' => Controller::class.'::string']); + + $this->assertSame(['GET', 'HEAD'], $route->getMethods()); + + $route = new Route('PUT', 'test', ['uses' => Controller::class.'::string']); + + $this->assertSame(['PUT'], $route->getMethods()); + + $route = new Route(['GET', 'POST'], 'test', ['uses' => Controller::class.'::string']); + + $this->assertSame(['GET', 'POST', 'HEAD'], $route->getMethods()); + } + + public function testGetDomain() + { + $route = new Route('GET', 'test', ['domain' => 'test.com']); + + $this->assertSame('test.com', $route->getDomain()); + } + + public function testGetAndSetUri() + { + $route = new Route('GET', 'test', ['domain' => 'test.com']); + + $this->assertSame('test', $route->getUri()); + + $route->setUri('/foo/bar'); + + $this->assertSame('/foo/bar', $route->getUri()); + } + + public function testGetAndSetName() + { + $route = new Route('GET', 'test', ['as' => 'test']); + + $this->assertSame('test', $route->getName()); + + $route->setName('foo'); + + $this->assertSame('testfoo', $route->getName()); + + $route = new Route('GET', 'test', null); + $route->setName('test'); + + $this->assertSame('test', $route->getName()); + } + + public function testHttpAndHttps() + { + $route = new Route('GET', 'test', ['http']); + + $this->assertTrue($route->isHttpOnly()); + + $route = new Route('GET', 'test', ['https']); + + $this->assertTrue($route->isHttpsOnly()); + } + + public function testSetAndGetPrefix() + { + $route = new Route('GET', 'test', ['prefix' => 'test']); + + $this->assertSame('test', $route->getPrefix()); + $this->assertSame('test/test', $route->getUri()); + + $route = new Route('GET', 'test', null); + $route->addPrefix('foo'); + + $this->assertSame('foo/test', $route->getUri()); + + $route->addPrefix('test'); + + $this->assertSame('test/foo/test', $route->getUri()); + } + + protected function getRouter() + { + return new Router($this->mock(ContainerInterface::class), new RouteParser()); + } +} diff --git a/src/Viserio/Routing/Tests/UrlGenerator/CachedDataGeneratorTest.php b/src/Viserio/Routing/Tests/UrlGenerator/CachedDataGeneratorTest.php deleted file mode 100644 index dede2e289..000000000 --- a/src/Viserio/Routing/Tests/UrlGenerator/CachedDataGeneratorTest.php +++ /dev/null @@ -1,63 +0,0 @@ -getGenerator($filename, $this->once()); - $data = $generator->getData(); - $this->assertNotEmpty(file_get_contents($filename)); - $this->assertEquals([], $data); - - return $filename; - } - - /** - * Test create with a fresh cache. - * - * @depends testCreateWritesCache - */ - public function testCreateWithFreshCache($filename) - { - $generator = $this->getGenerator($filename, $this->never()); - $generator->getData(); - } - - /** - * Test an unwriteable file. - * - * @todo This relies on something outside of Narrowspark throwing the exception - */ - public function testUnableToWriteCache() - { - $generator = $this->getGenerator('/some/unwriteable/path'); - $this->setExpectedException('RuntimeException', 'Failed to create'); - $generator->getData(); - } - - /** - * @param string $filename - * @param null $expects - * @param array $routes - * - * @return CachedDataGenerator - */ - protected function getGenerator($filename, $expects = null, $routes = []) - { - $generator = $this->getMockForAbstractClass('Viserio\Contracts\Routing\DataGenerator'); - $generator->expects($expects ?: $this->any()) - ->method('getData') - ->will($this->returnValue($routes)); - - return new CachedDataGenerator(new Filesystem(), $generator, $filename, false); - } -} diff --git a/src/Viserio/Routing/Tests/UrlGenerator/GroupCountBasedDataGeneratorTest.php b/src/Viserio/Routing/Tests/UrlGenerator/GroupCountBasedDataGeneratorTest.php deleted file mode 100644 index cd6fa6db2..000000000 --- a/src/Viserio/Routing/Tests/UrlGenerator/GroupCountBasedDataGeneratorTest.php +++ /dev/null @@ -1,105 +0,0 @@ -getMockForAbstractClass('Viserio\Contracts\Routing\RouteCollector'); - $collector->expects($this->once()) - ->method('getData') - ->will($this->returnValue([ - // Static routes - [ - '/' => [ - 'GET' => [ - 'name' => 'home', - 'controller' => 'handler1', - ], - ], - ], - // Dynamic routes - [ - 'GET' => [ - [ - 'regex' => '~^(?|/user/([^/]+)/show)$~', - 'routeMap' => [ - 2 => [ - [ - 'name' => 'user_show', - 'handler' => 'handler2', - ], - [ - 'id' => 'id', - ], - ], - ], - ], - ], - ], - ])); - - $generator = new GroupCountBasedDataGenerator($collector); - $data = $generator->getData(); - $this->assertEquals([ - 'home' => '/', - 'user_show' => [ - 'path' => '/user/{id}/show', - 'params' => [ - 'id' => 'id', - ], - ], - ], $data); - } - - /** - * Test invalid data handling. - */ - public function testInvalidData() - { - $collector = $this->getMockForAbstractClass('Viserio\Contracts\Routing\RouteCollector'); - $collector->expects($this->once()) - ->method('getData') - ->will($this->returnValue([ - [], - [ - 'GET' => [ - [ - 'regex' => '~^(?|/user/([^/]+)/show|/user/([^/]+)/edit)$~', - 'routeMap' => [ - // Invalid index - 0 => [ - [ - 'name' => 'user_show', - 'handler' => 'handler2', - ], - [ - 'id' => 'id', - ], - ], - // Valid, but No "name" attribute - 3 => [ - [ - 'handler' => 'handler2', - ], - [ - 'id' => 'id', - ], - ], - ], - ], - ], - ], - ])); - - $generator = new GroupCountBasedDataGenerator($collector); - $data = $generator->getData(); - $this->assertEquals([], $data); - } -} diff --git a/src/Viserio/Routing/Tests/UrlGenerator/SimpleUrlGeneratorTest.php b/src/Viserio/Routing/Tests/UrlGenerator/SimpleUrlGeneratorTest.php deleted file mode 100644 index ae6e8717a..000000000 --- a/src/Viserio/Routing/Tests/UrlGenerator/SimpleUrlGeneratorTest.php +++ /dev/null @@ -1,89 +0,0 @@ -getGenerator(); - $this->assertEquals('/', $generator->generate('home')); - } - - /** - * Test base URL functionality. - */ - public function testBaseUrl() - { - $generator = $this->getGenerator(); - $request = Request::create('https://www.example.com/subdirectory/somepage', 'GET', [], [], [], [ - 'SCRIPT_FILENAME' => 'index.php', - 'PHP_SELF' => '/subdirectory/index.php', - ]); - $generator->setRequest($request); - $this->assertEquals('/subdirectory/', $generator->generate('home')); - } - - /** - * Test absolute URL functionality. - */ - public function testAbsoluteUrl() - { - $generator = $this->getGenerator(); - $request = Request::create('https://www.example.com/subdirectory/somepage', 'GET', [], [], [], [ - 'SCRIPT_FILENAME' => 'index.php', - 'PHP_SELF' => '/subdirectory/index.php', - ]); - $generator->setRequest($request); - $this->assertEquals('https://www.example.com/subdirectory/', $generator->generate('home', [], true)); - } - - /** - * Test a dynamic route. - */ - public function testDynamicRoute() - { - $generator = $this->getGenerator([ - 'user_edit' => [ - 'params' => [ - 'id', - ], - 'path' => '/user/{id}/edit', - ], - ]); - $this->assertEquals('/user/123/edit', $generator->generate('user_edit', ['id' => 123])); - } - - /** - * Test a dynamic route with a missing parameter. - */ - public function testDynamicRouteWithMissingParameter() - { - $generator = $this->getGenerator([ - 'user_edit' => [ - 'params' => [ - 'id', - ], - 'path' => '/user/{id}/edit', - ], - ]); - $this->setExpectedException('RuntimeException', 'Missing required parameter'); - $this->assertEquals('/user/123/edit', $generator->generate('user_edit')); - } - - private function getGenerator(array $routes = ['home' => '/']) - { - $dataGenerator = $this->getMockForAbstractClass('Viserio\Contracts\Routing\DataGenerator'); - $dataGenerator->expects($this->once()) - ->method('getData') - ->will($this->returnValue($routes)); - - return new SimpleUrlGenerator($dataGenerator); - } -} diff --git a/src/Viserio/Routing/Tests/VarExporterTest.php b/src/Viserio/Routing/Tests/VarExporterTest.php new file mode 100644 index 000000000..59267c4f0 --- /dev/null +++ b/src/Viserio/Routing/Tests/VarExporterTest.php @@ -0,0 +1,48 @@ + 1]'], + [[1, 2, 3], '[0 => 1,1 => 2,2 => 3,]'], + [[1, '2', 3], '[0 => 1,1 => \'2\',2 => 3,]'], + [['foo' => 1, [2, 3]], '[\'foo\' => 1,0 => [0 => 2,1 => 3,],]'], + [new StdClass(), '(object)[]'], + [(object) ['foo' => 'bar'], '(object)[\'foo\' => \'bar\']'], + [new Controller(), 'unserialize(\'O:40:"Viserio\\\\Routing\\\\Tests\\\\Fixture\\\\Controller":0:{}\')'], + ]; + } + + /** + * @dataProvider exportCases + */ + public function testConvertsValueToValidPhp($value, $code) + { + $exported = VarExporter::export($value); + $evaluated = eval('return ' . $exported . ';'); + + $this->assertSame($code, $exported, ''); + $this->assertEquals($value, $evaluated); + } +} diff --git a/src/Viserio/Routing/UrlGenerator.php b/src/Viserio/Routing/UrlGenerator.php new file mode 100644 index 000000000..ab604a0c2 --- /dev/null +++ b/src/Viserio/Routing/UrlGenerator.php @@ -0,0 +1,28 @@ + '/', + '%40' => '@', + '%3A' => ':', + '%3B' => ';', + '%2C' => ',', + '%3D' => '=', + '%2B' => '+', + '%21' => '!', + '%2A' => '*', + '%7C' => '|', + '%3F' => '?', + '%26' => '&', + '%23' => '#', + '%25' => '%', + ]; +} diff --git a/src/Viserio/Routing/UrlGenerator/CachedDataGenerator.php b/src/Viserio/Routing/UrlGenerator/CachedDataGenerator.php deleted file mode 100644 index 7a3af4561..000000000 --- a/src/Viserio/Routing/UrlGenerator/CachedDataGenerator.php +++ /dev/null @@ -1,71 +0,0 @@ -wrappedGenerator = $wrappedGenerator; - $this->cacheFile = $cacheFile; - $this->debug = $debug; - - $this->files = $files; - } - - /** - * Get formatted route data for use by a URL generator. - * - * @return array - */ - public function getData(): array - { - $files = $this->files; - $cache = $this->cacheFile; - - if (! $files->exists($cache) || ! $this->debug) { - $routes = $this->wrappedGenerator->getData(); - $files->write($cache, 'getRequire($this->cacheFile); - } -} diff --git a/src/Viserio/Routing/UrlGenerator/GroupCountBasedDataGenerator.php b/src/Viserio/Routing/UrlGenerator/GroupCountBasedDataGenerator.php deleted file mode 100644 index 7c0a84e93..000000000 --- a/src/Viserio/Routing/UrlGenerator/GroupCountBasedDataGenerator.php +++ /dev/null @@ -1,105 +0,0 @@ -routeCollector = $routeCollector; - } - - /** - * Get formatted route data for use by a URL generator. - * - * @return array - */ - public function getData(): array - { - $routes = $this->routeCollector->getData(); - $data = []; - - foreach ($routes[0] as $path => $methods) { - $handler = reset($methods); - if (is_array($handler) && isset($handler['name'])) { - $data[$handler['name']] = $path; - } - } - - foreach ($routes[1] as $method) { - foreach ($method as $group) { - $data = array_merge($data, $this->parseDynamicGroup($group)); - } - } - - return $data; - } - - /** - * Parse a group of dynamic routes. - * - * @param $group - * - * @return array - */ - private function parseDynamicGroup($group) - { - $regex = $group['regex']; - $parts = explode('|', $regex); - $data = []; - - foreach ($group['routeMap'] as $matchIndex => $routeData) { - if (! is_array($routeData[0]) || ! isset($routeData[0]['name']) || ! isset($parts[$matchIndex - 1])) { - continue; - } - - $parameters = $routeData[1]; - $path = $parts[$matchIndex - 1]; - - foreach ($parameters as $parameter) { - $path = $this->replaceOnce('([^/]+)', '{' . $parameter . '}', $path); - } - - $path = rtrim($path, '()$~'); - $data[$routeData[0]['name']] = [ - 'path' => $path, - 'params' => $parameters, - ]; - } - - return $data; - } - - /** - * Replace the first occurrence of a string. - * - * @param string $search - * @param string $replace - * @param string $subject - * - * @return mixed - */ - private function replaceOnce($search, $replace, $subject) - { - $pos = strpos($subject, $search); - - if ($pos !== false) { - $subject = substr_replace($subject, $replace, $pos, strlen($search)); - } - - return $subject; - } -} diff --git a/src/Viserio/Routing/UrlGenerator/SimpleUrlGenerator.php b/src/Viserio/Routing/UrlGenerator/SimpleUrlGenerator.php deleted file mode 100644 index f45e7b9c4..000000000 --- a/src/Viserio/Routing/UrlGenerator/SimpleUrlGenerator.php +++ /dev/null @@ -1,96 +0,0 @@ -dataGenerator = $dataGenerator; - } - - /** - * Generate a URL for the given route. - * - * @param string $name The name of the route to generate a url for - * @param array $parameters Parameters to pass to the route - * @param bool $absolute If true, the generated route should be absolute - * - * @return string - */ - public function generate(string $name, array $parameters = [], bool $absolute = false): string - { - if (! $this->initialized) { - $this->initialize(); - } - - $alias = strpos($name, '@') === false ? '@' . $name : $name; - - $path = $this->routes[$alias]; - - if (is_array($path)) { - $params = $path['params']; - $path = $path['path']; - - foreach ($params as $param) { - if (! isset($parameters[$param])) { - throw new RuntimeException( - 'Missing required parameter "' . $param . '". Optional parameters not currently supported' - ); - } - - $path = str_replace('{' . $param . '}', $parameters[$param], $path); - } - } - - if ($this->request) { - $path = $this->request->getBaseUrl() . $path; - if ($absolute) { - $path = $this->request->getSchemeAndHttpHost() . $path; - } - } - - return $path; - } - - /** - * @param null|\Symfony\Component\HttpFoundation\Request $request - */ - public function setRequest(SymfonyRequest $request = null) - { - $this->request = $request; - } - - /** - * Initialize the generator. - */ - protected function initialize() - { - $this->routes = $this->dataGenerator->getData(); - $this->initialized = true; - } -} diff --git a/src/Viserio/Routing/VarExporter.php b/src/Viserio/Routing/VarExporter.php new file mode 100644 index 000000000..a8e1b268f --- /dev/null +++ b/src/Viserio/Routing/VarExporter.php @@ -0,0 +1,62 @@ + ' . self::export(current($value)) . ']'; + } + + $code = '['; + + foreach ($value as $key => $element) { + $code .= self::export($key); + $code .= ' => '; + $code .= self::export($element); + $code .= ','; + } + + $code .= ']'; + + return $code; + } elseif (is_object($value) && $value instanceof StdClass) { + return '(object)' . self::export((array) $value); + } + + if (is_scalar($value)) { + return var_export($value, true); + } + + return 'unserialize(' . var_export(serialize($value), true) . ')'; + } + + /** + * Don't instantiate this class. + * + * @codeCoverageIgnore + */ + private function __construct() { + // + } +} diff --git a/src/Viserio/Routing/composer.json b/src/Viserio/Routing/composer.json index 56bd1cb9d..7ed8f39e6 100644 --- a/src/Viserio/Routing/composer.json +++ b/src/Viserio/Routing/composer.json @@ -2,7 +2,7 @@ "name" : "viserio/routing", "type" : "library", "description": "The Viserio Routing package.", - "keywords" : ["viserio", "narrowspark", "route", "FastRoute", "dispatcher"], + "keywords" : ["viserio", "narrowspark", "route", "dispatcher", "router"], "license" : "MIT", "homepage" : "http://github.com/narrowspark/framework", "support" : { @@ -19,17 +19,19 @@ ], "require": { "php" : "7.0.0 - 7.0.5 || ^7.0.7", - "viserio/container" : "self.version", - "viserio/contracts" : "self.version", - "viserio/http" : "self.version", "container-interop/container-interop" : "^1.0", - "nikic/fast-route" : "^0.5", - "symfony/http-kernel" : "^3.1" + "narrowspark/arr" : "^1.1", + "php-di/invoker" : "^1.3", + "psr/http-message" : "^1.0", + "viserio/contracts" : "self.version", + "viserio/middleware" : "self.version", + "viserio/support" : "self.version" }, "require-dev": { "narrowspark/php-cs-fixer-config" : "^1.1", "narrowspark/testing-helper" : "^1.5", - "phpunit/phpunit" : "^5.1" + "phpunit/phpunit" : "^5.1", + "viserio/http" : "self.version" }, "autoload": { "psr-4": { diff --git a/src/Viserio/StaticalProxy/StaticalProxy.php b/src/Viserio/StaticalProxy/StaticalProxy.php index 587cfb15f..4514bde6f 100644 --- a/src/Viserio/StaticalProxy/StaticalProxy.php +++ b/src/Viserio/StaticalProxy/StaticalProxy.php @@ -154,7 +154,7 @@ protected static function resolveStaticalProxyInstance($name) * * @param string $name * - * @return object + * @return MockInterface */ protected static function createFreshMockInstance(string $name) { @@ -168,7 +168,7 @@ protected static function createFreshMockInstance(string $name) /** * Create a fresh mock instance for the given class. * - * @return object + * @return MockInterface */ protected static function createMock() { diff --git a/src/Viserio/Support/AbstractConnectionManager.php b/src/Viserio/Support/AbstractConnectionManager.php index ffe881105..06cfa0214 100644 --- a/src/Viserio/Support/AbstractConnectionManager.php +++ b/src/Viserio/Support/AbstractConnectionManager.php @@ -7,9 +7,9 @@ use InvalidArgumentException; use Viserio\Contracts\{ Config\Manager as ConfigContract, + Container\Traits\ContainerAwareTrait, Support\Connector as ConnectorContract }; -use Viserio\Support\Traits\ContainerAwareTrait; abstract class AbstractConnectionManager { diff --git a/src/Viserio/Support/AbstractManager.php b/src/Viserio/Support/AbstractManager.php index 6a72c916e..6e39def21 100644 --- a/src/Viserio/Support/AbstractManager.php +++ b/src/Viserio/Support/AbstractManager.php @@ -4,8 +4,10 @@ use Closure; use InvalidArgumentException; -use Viserio\Contracts\Config\Manager as ConfigContract; -use Viserio\Support\Traits\ContainerAwareTrait; +use Viserio\Contracts\{ + Config\Manager as ConfigContract, + Container\Traits\ContainerAwareTrait +}; abstract class AbstractManager { diff --git a/src/Viserio/Support/Debug/Dumper.php b/src/Viserio/Support/Debug/Dumper.php index 73c2d93c4..b70fbdd3e 100644 --- a/src/Viserio/Support/Debug/Dumper.php +++ b/src/Viserio/Support/Debug/Dumper.php @@ -2,8 +2,10 @@ declare(strict_types=1); namespace Viserio\Support\Debug; -use Symfony\Component\VarDumper\Cloner\VarCloner; -use Symfony\Component\VarDumper\Dumper\CliDumper; +use Symfony\Component\VarDumper\{ + Cloner\VarCloner, + Dumper\CliDumper +}; /** * @codeCoverageIgnore diff --git a/src/Viserio/Support/Invoker.php b/src/Viserio/Support/Invoker.php index 5e51036ad..441f4e2a9 100644 --- a/src/Viserio/Support/Invoker.php +++ b/src/Viserio/Support/Invoker.php @@ -4,13 +4,15 @@ use Invoker\Invoker as DiInvoker; use Invoker\InvokerInterface; -use Invoker\ParameterResolver\AssociativeArrayResolver; -use Invoker\ParameterResolver\Container\ParameterNameContainerResolver; -use Invoker\ParameterResolver\Container\TypeHintContainerResolver; -use Invoker\ParameterResolver\DefaultValueResolver; -use Invoker\ParameterResolver\NumericArrayResolver; -use Invoker\ParameterResolver\ResolverChain; -use Viserio\Support\Traits\ContainerAwareTrait; +use Invoker\ParameterResolver\{ + AssociativeArrayResolver, + Container\ParameterNameContainerResolver, + Container\TypeHintContainerResolver, + DefaultValueResolver, + NumericArrayResolver, + ResolverChain +}; +use Viserio\Contracts\Container\Traits\ContainerAwareTrait; class Invoker implements InvokerInterface { diff --git a/src/Viserio/Support/composer.json b/src/Viserio/Support/composer.json index cc4ca87fd..1bd801830 100644 --- a/src/Viserio/Support/composer.json +++ b/src/Viserio/Support/composer.json @@ -18,42 +18,38 @@ } ], "require": { - "php" : "7.0.0 - 7.0.5 || ^7.0.7", - "viserio/contracts" : "self.version" + "php" : "7.0.0 - 7.0.5 || ^7.0.7", + "viserio/contracts" : "self.version" }, "require-dev": { - "danielstjules/stringy" : "^2.3", - "narrowspark/php-cs-fixer-config" : "^1.1", - "narrowspark/testing-helper" : "^1.5", - "php-di/invoker" : "^1.3", - "phpunit/phpunit" : "^5.1", - "symfony/var-dumper" : "^3.1" + "danielstjules/stringy" : "^2.3", + "narrowspark/php-cs-fixer-config" : "^1.1", + "narrowspark/testing-helper" : "^1.5", + "php-di/invoker" : "^1.3", + "phpunit/phpunit" : "^5.1", + "symfony/var-dumper" : "^3.1" }, "autoload": { "psr-4": { - "Viserio\\Support\\" : "" + "Viserio\\Support\\" : "" }, - "exclude-from-classmap" : ["/Tests/"] + "exclude-from-classmap" : ["/Tests/"] }, "autoload-dev": { "psr-4": { - "Viserio\\Support\\Tests\\" : "Tests/" + "Viserio\\Support\\Tests\\" : "Tests/" } }, - "provide": { - "container-interop/container-interop-implementation" : "~1.1", - "psr/log-implementation" : "~1.0" - }, "extra": { "branch-alias": { - "dev-master" : "1.0-dev" + "dev-master" : "1.0-dev" } }, "suggest": { - "danielstjules/stringy" : "Required to use the Stringy Class in Str (^2.3)", - "php-di/invoker" : "Required to use the Invoker Class (^1.3)", - "symfony/var-dumper" : "Improves the Dumper::dump() (^3.1)." + "danielstjules/stringy" : "Required to use the Stringy Class in Str (^2.3)", + "php-di/invoker" : "Required to use the Invoker Class (^1.3)", + "symfony/var-dumper" : "Improves the Dumper::dump() (^3.1)." }, - "minimum-stability" : "dev", - "prefer-stable" : true + "minimum-stability" : "dev", + "prefer-stable" : true } diff --git a/src/Viserio/View/Virtuoso.php b/src/Viserio/View/Virtuoso.php index a8a706acf..922f09329 100644 --- a/src/Viserio/View/Virtuoso.php +++ b/src/Viserio/View/Virtuoso.php @@ -6,14 +6,14 @@ use Interop\Container\ContainerInterface; use InvalidArgumentException; use Viserio\Contracts\{ + Container\Traits\ContainerAwareTrait, Events\Dispatcher as DispatcherContract, View\View as ViewContract, View\Virtuoso as VirtuosoContract }; use Viserio\Support\{ Invoker, - Str, - Traits\ContainerAwareTrait + Str }; use Viserio\View\Traits\NormalizeNameTrait;