diff --git a/src/App.php b/src/App.php index f81c352..5053b5a 100755 --- a/src/App.php +++ b/src/App.php @@ -298,54 +298,6 @@ public static function setMode(string $value): void self::$mode = $value; } - /** - * If a resource has been created return it, otherwise create it and then return it - * - * @param string $name - * @param bool $fresh - * @return mixed - * - * @throws Exception - */ - public function getResource(string $name, bool $fresh = false): mixed - { - if ($name === 'utopia') { - return $this; - } - - if (!\array_key_exists($name, $this->resources) || $fresh || self::$resourcesCallbacks[$name]['reset']) { - if (!\array_key_exists($name, self::$resourcesCallbacks)) { - throw new Exception('Failed to find resource: "' . $name . '"'); - } - - $this->resources[$name] = \call_user_func_array( - self::$resourcesCallbacks[$name]['callback'], - $this->getResources(self::$resourcesCallbacks[$name]['injections']) - ); - } - - self::$resourcesCallbacks[$name]['reset'] = false; - - return $this->resources[$name]; - } - - /** - * Get Resources By List - * - * @param array $list - * @return array - */ - public function getResources(array $list): array - { - $resources = []; - - foreach ($list as $name) { - $resources[$name] = $this->getResource($name); - } - - return $resources; - } - /** * Set a new resource callback * @@ -476,7 +428,7 @@ public function match(Request $request, bool $fresh = false): ?Route * @param Route $route * @param Request $request */ - public function execute(Route $route, Request $request, Response $response): static + public function execute(Transaction $transaction, Route $route, Request $request, Response $response): static { $arguments = []; $groups = $route->getGroups(); @@ -486,7 +438,7 @@ public function execute(Route $route, Request $request, Response $response): sta if ($route->getHook()) { foreach (self::$init as $hook) { // Global init hooks if (in_array('*', $hook->getGroups())) { - $arguments = $this->getArguments($hook, $pathValues, $request->getParams()); + $arguments = $this->getArguments($transaction, $hook, $pathValues, $request->getParams()); \call_user_func_array($hook->getAction(), $arguments); } } @@ -495,14 +447,14 @@ public function execute(Route $route, Request $request, Response $response): sta foreach ($groups as $group) { foreach (self::$init as $hook) { // Group init hooks if (in_array($group, $hook->getGroups())) { - $arguments = $this->getArguments($hook, $pathValues, $request->getParams()); + $arguments = $this->getArguments($transaction, $hook, $pathValues, $request->getParams()); \call_user_func_array($hook->getAction(), $arguments); } } } if (!($response->isSent())) { - $arguments = $this->getArguments($route, $pathValues, $request->getParams()); + $arguments = $this->getArguments($transaction, $route, $pathValues, $request->getParams()); // Call the action callback with the matched positions as params \call_user_func_array($route->getAction(), $arguments); @@ -512,7 +464,7 @@ public function execute(Route $route, Request $request, Response $response): sta foreach ($groups as $group) { foreach (self::$shutdown as $hook) { // Group shutdown hooks if (in_array($group, $hook->getGroups())) { - $arguments = $this->getArguments($hook, $pathValues, $request->getParams()); + $arguments = $this->getArguments($transaction, $hook, $pathValues, $request->getParams()); \call_user_func_array($hook->getAction(), $arguments); } } @@ -521,19 +473,20 @@ public function execute(Route $route, Request $request, Response $response): sta if ($route->getHook()) { foreach (self::$shutdown as $hook) { // Group shutdown hooks if (in_array('*', $hook->getGroups())) { - $arguments = $this->getArguments($hook, $pathValues, $request->getParams()); + $arguments = $this->getArguments($transaction, $hook, $pathValues, $request->getParams()); \call_user_func_array($hook->getAction(), $arguments); } } } } catch (\Throwable $e) { self::setResource('error', fn () => $e); + $transaction->setResource('error', fn () => $e); foreach ($groups as $group) { foreach (self::$errors as $error) { // Group error hooks if (in_array($group, $error->getGroups())) { try { - $arguments = $this->getArguments($error, $pathValues, $request->getParams()); + $arguments = $this->getArguments($transaction, $error, $pathValues, $request->getParams()); \call_user_func_array($error->getAction(), $arguments); } catch (\Throwable $e) { throw new Exception('Error handler had an error: ' . $e->getMessage(), 500, $e); @@ -545,7 +498,7 @@ public function execute(Route $route, Request $request, Response $response): sta foreach (self::$errors as $error) { // Global error hooks if (in_array('*', $error->getGroups())) { try { - $arguments = $this->getArguments($error, $pathValues, $request->getParams()); + $arguments = $this->getArguments($transaction, $error, $pathValues, $request->getParams()); \call_user_func_array($error->getAction(), $arguments); } catch (\Throwable $e) { throw new Exception('Error handler had an error: ' . $e->getMessage(), 500, $e); @@ -567,7 +520,7 @@ public function execute(Route $route, Request $request, Response $response): sta * * @throws Exception */ - protected function getArguments(Hook $hook, array $values, array $requestParams): array + protected function getArguments(Transaction $transaction, Hook $hook, array $values, array $requestParams): array { $arguments = []; foreach ($hook->getParams() as $key => $param) { // Get value from route or request object @@ -584,7 +537,7 @@ protected function getArguments(Hook $hook, array $values, array $requestParams) } if ($paramExists) { - $this->validate($key, $param, $value); + $this->validate($transaction, $key, $param, $value); } } @@ -593,12 +546,41 @@ protected function getArguments(Hook $hook, array $values, array $requestParams) } foreach ($hook->getInjections() as $key => $injection) { - $arguments[$injection['order']] = $this->getResource($injection['name']); + $arguments[$injection['order']] = $transaction->getResource($injection['name']); } return $arguments; } + public function createTransaction(Request $request, Response $response) + { + $transaction = new Transaction(); + + $transaction->setResource('request', function () use ($request) { + return $request; + }); + + $transaction->setResource('response', function () use ($response) { + return $response; + }); + + foreach (self::$resourcesCallbacks as $name => $value) { + $transaction->setResource($name, $value['callback'], $value['injections']); + } + + $utopia = $this; + $transaction->setResource('utopia', function () use ($utopia) { + return $utopia; + }); + + $route = $this->match($request); + $transaction->setResource('route', function () use ($route) { + return $route; + }); + + return $transaction; + } + /** * Run * @@ -607,19 +589,16 @@ protected function getArguments(Hook $hook, array $values, array $requestParams) * * @param Request $request * @param Response $response + * @param ?Transaction $transaction */ - public function run(Request $request, Response $response): static + public function run(Request $request, Response $response, Transaction $transaction = null): static { - $this->resources['request'] = $request; - $this->resources['response'] = $response; - - self::setResource('request', function () use ($request) { - return $request; - }); - - self::setResource('response', function () use ($response) { - return $response; - }); + if(empty($transaction)) { + $transaction = $this->createTransaction($request, $response); + } else { + $transaction->setResource('request', fn () => $request); + $transaction->setResource('response', fn () => $response); + } $method = $request->getMethod(); $route = $this->match($request); @@ -636,7 +615,7 @@ public function run(Request $request, Response $response): static foreach (self::$options as $option) { // Group options hooks /** @var Hook $option */ if (in_array($group, $option->getGroups())) { - \call_user_func_array($option->getAction(), $this->getArguments($option, [], $request->getParams())); + \call_user_func_array($option->getAction(), $this->getArguments($transaction, $option, [], $request->getParams())); } } } @@ -644,7 +623,7 @@ public function run(Request $request, Response $response): static foreach (self::$options as $option) { // Global options hooks /** @var Hook $option */ if (in_array('*', $option->getGroups())) { - \call_user_func_array($option->getAction(), $this->getArguments($option, [], $request->getParams())); + \call_user_func_array($option->getAction(), $this->getArguments($transaction, $option, [], $request->getParams())); } } } catch (\Throwable $e) { @@ -654,7 +633,10 @@ public function run(Request $request, Response $response): static self::setResource('error', function () use ($e) { return $e; }); - \call_user_func_array($error->getAction(), $this->getArguments($error, [], $request->getParams())); + $transaction->setResource('error', function () use ($e) { + return $e; + }); + \call_user_func_array($error->getAction(), $this->getArguments($transaction, $error, [], $request->getParams())); } } } @@ -667,23 +649,27 @@ public function run(Request $request, Response $response): static $this->route = $route; $path = \parse_url($request->getURI(), PHP_URL_PATH); $route->path($path); + + $transaction->setResource('route', function () use ($route) { + return $route; + }); } if (null !== $route) { - return $this->execute($route, $request, $response); + return $this->execute($transaction, $route, $request, $response); } elseif (self::REQUEST_METHOD_OPTIONS == $method) { try { foreach ($groups as $group) { foreach (self::$options as $option) { // Group options hooks if (in_array($group, $option->getGroups())) { - \call_user_func_array($option->getAction(), $this->getArguments($option, [], $request->getParams())); + \call_user_func_array($option->getAction(), $this->getArguments($transaction, $option, [], $request->getParams())); } } } foreach (self::$options as $option) { // Global options hooks if (in_array('*', $option->getGroups())) { - \call_user_func_array($option->getAction(), $this->getArguments($option, [], $request->getParams())); + \call_user_func_array($option->getAction(), $this->getArguments($transaction, $option, [], $request->getParams())); } } } catch (\Throwable $e) { @@ -692,7 +678,7 @@ public function run(Request $request, Response $response): static self::setResource('error', function () use ($e) { return $e; }); - \call_user_func_array($error->getAction(), $this->getArguments($error, [], $request->getParams())); + \call_user_func_array($error->getAction(), $this->getArguments($transaction, $error, [], $request->getParams())); } } } @@ -702,7 +688,10 @@ public function run(Request $request, Response $response): static self::setResource('error', function () { return new Exception('Not Found', 404); }); - \call_user_func_array($error->getAction(), $this->getArguments($error, [], $request->getParams())); + $transaction->setResource('error', function () { + return new Exception('Not Found', 404); + }); + \call_user_func_array($error->getAction(), $this->getArguments($transaction, $error, [], $request->getParams())); } } } @@ -722,7 +711,7 @@ public function run(Request $request, Response $response): static * * @throws Exception */ - protected function validate(string $key, array $param, mixed $value): void + protected function validate(Transaction $transaction, string $key, array $param, mixed $value): void { if ($param['optional'] && \is_null($value)) { return; @@ -731,7 +720,7 @@ protected function validate(string $key, array $param, mixed $value): void $validator = $param['validator']; // checking whether the class exists if (\is_callable($validator)) { - $validator = \call_user_func_array($validator, $this->getResources($param['injections'])); + $validator = \call_user_func_array($validator, $transaction->getResources($param['injections'])); } if (!$validator instanceof Validator) { // is the validator object an instance of the Validator class diff --git a/src/Transaction.php b/src/Transaction.php new file mode 100755 index 0000000..5198a9c --- /dev/null +++ b/src/Transaction.php @@ -0,0 +1,65 @@ +id = $this->unique(); + $this->resourcesCallbacks = []; + $this->resources = []; + } + + public function setResource(string $name, callable $callback, array $injections = []): void + { + $this->resourcesCallbacks[$name] = ['callback' => $callback, 'injections' => $injections]; + } + + public function getResource(string $name, bool $fresh = false): mixed + { + if ($name === 'transaction') { + return $this; + } + + if (!\array_key_exists($name, $this->resources) || $fresh) { + if (!\array_key_exists($name, $this->resourcesCallbacks)) { + throw new Exception('Failed to find resource: "' . $name . '"'); + } + + $this->resources[$name] = \call_user_func_array( + $this->resourcesCallbacks[$name]['callback'], + $this->getResources($this->resourcesCallbacks[$name]['injections']) + ); + } + + return $this->resources[$name]; + } + + public function getResources(array $list): array + { + $resources = []; + + foreach ($list as $name) { + $resources[$name] = $this->getResource($name); + } + + return $resources; + } + + protected static function unique(int $padding = 7): string + { + $uniqid = \uniqid(); + + if ($padding > 0) { + $bytes = \random_bytes((int) \ceil($padding / 2)); // one byte expands to two chars + $uniqid .= \substr(\bin2hex($bytes), 0, $padding); + } + + return $uniqid; + } +} diff --git a/tests/AppTest.php b/tests/AppTest.php index 5f08054..a3a3cd3 100755 --- a/tests/AppTest.php +++ b/tests/AppTest.php @@ -83,17 +83,21 @@ public function testCanGetResources(): void App::setResource('first', fn ($second) => "first-{$second}", ['second']); App::setResource('second', fn () => 'second'); - $second = $this->app->getResource('second'); - $first = $this->app->getResource('first'); + $request = new Request(); + $response = new Response(); + $transaction = $this->app->createTransaction($request, $response); + + $second = $transaction->getResource('second'); + $first = $transaction->getResource('first'); $this->assertEquals('second', $second); $this->assertEquals('first-second', $first); - $resource = $this->app->getResource('rand'); + $resource = $transaction->getResource('rand'); $this->assertNotEmpty($resource); - $this->assertEquals($resource, $this->app->getResource('rand')); - $this->assertEquals($resource, $this->app->getResource('rand')); - $this->assertEquals($resource, $this->app->getResource('rand')); + $this->assertEquals($resource, $transaction->getResource('rand')); + $this->assertEquals($resource, $transaction->getResource('rand')); + $this->assertEquals($resource, $transaction->getResource('rand')); // Default Params $route = new Route('GET', '/path'); @@ -107,7 +111,7 @@ public function testCanGetResources(): void }); \ob_start(); - $this->app->execute($route, new Request(), new Response()); + $this->app->execute($transaction, $route, $request, $response); $result = \ob_get_contents(); \ob_end_clean(); @@ -117,7 +121,12 @@ public function testCanGetResources(): void public function testCanExecuteRoute(): void { App::setResource('rand', fn () => rand()); - $resource = $this->app->getResource('rand'); + + $request = new Request(); + $response = new Response(); + $transaction = $this->app->createTransaction($request, $response); + + $resource = $transaction->getResource('rand'); $this->app ->error() @@ -137,7 +146,7 @@ public function testCanExecuteRoute(): void }); \ob_start(); - $this->app->execute($route, new Request(), new Response()); + $this->app->execute($transaction, $route, $request, $response); $result = \ob_get_contents(); \ob_end_clean(); @@ -161,7 +170,9 @@ public function testCanExecuteRoute(): void \ob_start(); $request = new UtopiaRequestTest(); $request::_setParams(['x' => 'param-x', 'y' => 'param-y', 'z' => 'param-z']); - $this->app->execute($route, $request, new Response()); + $response = new Response(); + $transaction->setResource('request', fn () => $request); + $this->app->execute($transaction, $route, $request, $response); $result = \ob_get_contents(); \ob_end_clean(); @@ -181,7 +192,8 @@ public function testCanExecuteRoute(): void \ob_start(); $request = new UtopiaRequestTest(); $request::_setParams(['x' => 'param-x', 'y' => 'param-y']); - $this->app->execute($route, $request, new Response()); + $response = new Response(); + $this->app->execute($this->app->createTransaction($request, $response), $route, $request, $response); $result = \ob_get_contents(); \ob_end_clean(); @@ -253,7 +265,8 @@ public function testCanExecuteRoute(): void \ob_start(); $request = new UtopiaRequestTest(); $request::_setParams(['x' => 'param-x', 'y' => 'param-y']); - $this->app->execute($route, $request, new Response()); + $transaction->setResource('request', fn () => $request); + $this->app->execute($transaction, $route, $request, $response); $result = \ob_get_contents(); \ob_end_clean(); @@ -262,7 +275,8 @@ public function testCanExecuteRoute(): void \ob_start(); $request = new UtopiaRequestTest(); $request::_setParams(['x' => 'param-x', 'y' => 'param-y']); - $this->app->execute($homepage, $request, new Response()); + $transaction->setResource('request', fn () => $request); + $this->app->execute($transaction, $homepage, $request, $response); $result = \ob_get_contents(); \ob_end_clean(); @@ -292,7 +306,9 @@ public function testCanAddAndExecuteHooks() }); \ob_start(); - $this->app->execute($route, new Request(), new Response()); + $request = new Request(); + $response = new Response(); + $this->app->execute($this->app->createTransaction($request, $response), $route, $request, $response); $result = \ob_get_contents(); \ob_end_clean(); @@ -308,7 +324,9 @@ public function testCanAddAndExecuteHooks() }); \ob_start(); - $this->app->execute($route, new Request(), new Response()); + $request = new Request(); + $response = new Response(); + $this->app->execute($this->app->createTransaction($request, $response), $route, $request, $response); $result = \ob_get_contents(); \ob_end_clean(); @@ -346,7 +364,9 @@ public function testCanHookThrowExceptions() }); \ob_start(); - $this->app->execute($route, new Request(), new Response()); + $request = new Request(); + $response = new Response(); + $this->app->execute($this->app->createTransaction($request, $response), $route, $request, $response); $result = \ob_get_contents(); \ob_end_clean(); @@ -354,7 +374,9 @@ public function testCanHookThrowExceptions() \ob_start(); $_GET['y'] = 'y-def'; - $this->app->execute($route, new Request(), new Response()); + $request = new Request(); + $response = new Response(); + $this->app->execute($this->app->createTransaction($request, $response), $route, $request, $response); $result = \ob_get_contents(); \ob_end_clean(); @@ -519,9 +541,10 @@ public function testWildcardRoute(): void App::init() ->inject('request') ->inject('response') - ->action(function (Request $request, Response $response) { - $route = $this->app->getRoute(); - App::setResource('myRoute', fn () => $route); + ->inject('route') + ->inject('transaction') + ->action(function (Request $request, Response $response, Route $route, Transaction $transaction) { + $transaction->setResource('myRoute', fn () => $route); if ($request->getURI() === '/init_response') { $response->send('THIS IS RESPONSE FROM INIT!'); @@ -583,4 +606,36 @@ public function testWildcardRoute(): void $_SERVER['REQUEST_METHOD'] = $method; $_SERVER['REQUEST_URI'] = $uri; } + + public function testRunWithTransaction(): void + { + $method = $_SERVER['REQUEST_METHOD'] ?? null; + $uri = $_SERVER['REQUEST_URI'] ?? null; + + $_SERVER['REQUEST_METHOD'] = 'GET'; + $_SERVER['REQUEST_URI'] = '/transaction-test'; + + $request = new Request(); + $response = new Response(); + $transaction = $this->app->createTransaction($request, $response); + + $transaction->setResource('myResource', fn () => 'myText'); + + App::get('/transaction-test') + ->inject('myResource') + ->inject('response') + ->action(function (mixed $myResource, $response) { + $response->send('Resource: ' . $myResource); + }); + + \ob_start(); + @$this->app->run($request, $response, $transaction); + $result = \ob_get_contents(); + \ob_end_clean(); + + $this->assertEquals('Resource: myText', $result); + + $_SERVER['REQUEST_METHOD'] = $method; + $_SERVER['REQUEST_URI'] = $uri; + } }