diff --git a/composer.json b/composer.json index 8ab54f3..8e166d1 100644 --- a/composer.json +++ b/composer.json @@ -11,12 +11,11 @@ } ], "require": { - "php": ">=8.4.0", - "symfony/http-foundation": "7.2.5", - "psr/container": "2.0" + "php": ">=7.2.5", + "symfony/http-foundation": "~5.4" }, "require-dev": { - "phpunit/phpunit": "12.1.2" + "phpunit/phpunit": "^9.5" }, "minimum-stability": "stable", "autoload": { @@ -26,3 +25,4 @@ "test": "phpunit" } } + diff --git a/index.php b/index.php deleted file mode 100644 index 6128911..0000000 --- a/index.php +++ /dev/null @@ -1,10 +0,0 @@ -get('hello/{name}', function ($name) { - - return 'Hello '.$name; -})->run(); \ No newline at end of file diff --git a/src/Adapter/SessionServiceAdapter.php b/src/Adapter/SessionServiceAdapter.php index 9f30aa4..f12d2b4 100644 --- a/src/Adapter/SessionServiceAdapter.php +++ b/src/Adapter/SessionServiceAdapter.php @@ -29,37 +29,38 @@ * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ -declare(strict_types=1); - namespace Mabs\Adapter; + use Mabs\Container\Container; use Mabs\ServiceAdapterInterface; use Mabs\Events; use Symfony\Component\HttpFoundation\Session\Storage\Handler\NativeFileSessionHandler; use Symfony\Component\HttpFoundation\Session\Storage\NativeSessionStorage; +use Symfony\Component\HttpFoundation\Session\Storage\MockFileSessionStorage; use Symfony\Component\HttpFoundation\Session\Session; -use Psr\Container\ContainerInterface; use Symfony\Component\HttpFoundation\Cookie; class SessionServiceAdapter implements ServiceAdapterInterface { - public function load(ContainerInterface $container): void + public function load(Container $container) { - $container['session.storage.options'] = []; + $container['session.storage.options'] = array(); $container['session.default_locale'] = 'en'; $container['session.storage.save_path'] = '/tmp'; - $container['session.storage.handler'] = fn(Container $c): NativeFileSessionHandler => - new NativeFileSessionHandler($c['session.storage.save_path']); + $container['session.storage.handler'] = function (Container $c) { + return new NativeFileSessionHandler($c['session.storage.save_path']); + }; - $container['session.storage.native'] = fn(Container $c): NativeSessionStorage => - new NativeSessionStorage( + $container['session.storage.native'] = function (Container $c) { + return new NativeSessionStorage( $c['session.storage.options'], $c['session.storage.handler'] ); + }; - $container['session'] = function (Container $app): Session { + $container['session'] = function (Container $app) { if (!isset($app['session.storage'])) { $app['session.storage'] = $app['session.storage.native']; } @@ -67,12 +68,12 @@ public function load(ContainerInterface $container): void }; } - public function boot(ContainerInterface $container): void + public function boot(Container $container) { - $container['event_dispatcher']->register(Events::MABS_ON_BOOT, [$this, 'onMabsBoot'], 128); + $container['event_dispatcher']->register(Events::MABS_ON_BOOT, array($this, 'onMabsBoot'), 128); } - public function onMabsBoot(Container $container): void + public function onMabsBoot(Container $container) { $container['request']->setSession($container['session']); } diff --git a/src/Application.php b/src/Application.php index caa2b09..d394e43 100644 --- a/src/Application.php +++ b/src/Application.php @@ -29,76 +29,113 @@ * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ -declare(strict_types=1); - namespace Mabs; -use Closure; use Mabs\Container\Container; use Mabs\Dispatcher\EventDispatcher; use Mabs\Router\Route; +use Mabs\Router\RouteCollection; use Mabs\Router\Router; use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpFoundation\Response; -use Mabs\Adapter\SessionServiceAdapter; -final class Application +class Application { - public const VERSION = '3.0.0'; + const VERSION = '2.1.1'; - private readonly Container $container; - private readonly \SplObjectStorage $adapters; - private bool $debug; - private bool $loaded = false; - private bool $booted = false; + protected $container; - public function __construct(bool $debug = false) - { - $this->debug = $debug; + protected $adapters; + + protected $debug; + + protected $loaded = false; - ini_set('display_errors', $debug ? 'on' : 'off'); - error_reporting($debug ? E_ALL : 0); + protected $booted = false; + public function __construct($debug = false) + { + if ($debug) { + ini_set('display_errors', 'on'); + error_reporting(E_ALL); + } else { + error_reporting(0); + } + $this->debug = $debug; $this->adapters = new \SplObjectStorage(); $this->container = new Container(); $this->load(); + $this->lock(); } - public function isDebugMode(): bool + /** + * check if debug mode is active + * @return bool + */ + public function isDebugMode() { - return $this->debug; + return $this->debug === true; } - public function on(string $eventName, Closure $callback, int $priority = 0): EventDispatcher + /** + * attach an action for an event + * @param string $eventName + * @param callable $callback + * @param int $priority + * @return \Mabs\EventDispatcher + */ + public function on($eventName, \Closure $callback, $priority = 0) { return $this->container['event_dispatcher']->register($eventName, $callback, $priority); } - public function detach(string $eventName): EventDispatcher + /** + * detach registered actions for an event + * @param string $eventName + * @return \Mabs\EventDispatcher + */ + public function detach($eventName) { return $this->container['event_dispatcher']->detach($eventName); } - public function dispatch(string $eventName, mixed $data = null): mixed + /** + * @param string $eventName + * @param mixed $data + * @return mixed + */ + public function dispatch($eventName, $data = null) { return $this->container['event_dispatcher']->dispatch($eventName, $data); } - public function isLoaded(): bool + /** + * check if all component are loaded + * @return bool + */ + public function isLoaded() { - return $this->loaded; + return $this->loaded === true; } - public function isBooted(): bool + /** + * check if all component are booted + * @return bool + */ + public function isBooted() { - return $this->booted; + return $this->booted === true; } - public function run(): void + /** + * run applicaton : handle the request and send response + */ + public function run() { try { + if (!$this->isLoaded()) { $this->load(); } @@ -110,7 +147,7 @@ public function run(): void $response = $this->handleRequest(); $this->dispatch(Events::MABS_ON_TERMINATE, $response); - } catch (\Throwable $e) { + } catch (\Exception $e) { $this->dispatch(Events::MABS_HANDLE_EXCEPTION, $e); $response = new Response('500 internal server error', 500); } @@ -119,72 +156,140 @@ public function run(): void $this->dispatch(Events::MABS_ON_FINISH); } - public function handleRequest(?Request $request = null): Response + /** + * handle a Request + * @param Request $request + * @return Response + */ + public function handleRequest(Request $request = null) { - $request ??= $this->container['request']; + if (!$request) { + $request = $this->container['request']; + } $this->dispatch(Events::MABS_HANDLE_REQUEST, $request); $response = $this->container['router']->handleRequest($request); - return $response instanceof Response ? $response : new Response((string)$response, 200); + if (! $response instanceof Response) { + $response = new Response($response, 200); + } + + return $response; } - public function get(string $pattern, Closure|string $callback, ?string $routeName = null): self + /** + * add GET route + * @param string $pattern + * @param \Closure|string $callback + * @param null|string $routeName + * @return Application + */ + public function get($pattern, $callback, $routeName = null) { - return $this->mount($pattern, $callback, $routeName, [Request::METHOD_GET]); + return $this->mount($pattern, $callback, $routeName, array(Request::METHOD_GET)); } - public function post(string $pattern, Closure|string $callback, ?string $routeName = null): self + /** + * add POST route + * @param string $pattern + * @param \Closure|string $callback + * @param null|string $routeName + * @return Application + */ + public function post($pattern, $callback, $routeName = null) { - return $this->mount($pattern, $callback, $routeName, [Request::METHOD_POST]); + return $this->mount($pattern, $callback, $routeName, array(Request::METHOD_POST)); } - public function put(string $pattern, Closure|string $callback, ?string $routeName = null): self + /** + * add PUT route + * @param string $pattern + * @param \Closure|string $callback + * @param null|string $routeName + * @return Application + */ + public function put($pattern, $callback, $routeName = null) { - return $this->mount($pattern, $callback, $routeName, [Request::METHOD_PUT]); + return $this->mount($pattern, $callback, $routeName, array(Request::METHOD_PUT)); } - public function delete(string $pattern, Closure|string $callback, ?string $routeName = null): self + /** + * add DELETE route + * @param string $pattern + * @param \Closure|string $callback + * @param null|string $routeName + * @return Application + */ + public function delete($pattern, $callback, $routeName = null) { - return $this->mount($pattern, $callback, $routeName, [Request::METHOD_DELETE]); + return $this->mount($pattern, $callback, $routeName, array(Request::METHOD_DELETE)); } - public function mount( - string $pattern, - Closure|string $callback, - ?string $routeName = null, - array $methods = [] - ): self { - $route = (new Route()) - ->setPath($pattern) + /** + * add a route + * @param string $pattern + * @param \Closure|string $callback + * @param null|string $routeName + * @param array HTTP Methode + * @return Application + */ + public function mount($pattern, $callback, $routeName = null, $methodes = array()) + { + $route = new Route(); + $route->setPath($pattern) ->setName($routeName) ->setCallback($callback); - $this->container['router']->mount($route, $methods); + $this->container['router']->mount($route, $methodes); + return $this; } - public function getContainer(): Container + /** + * get the DI container + * @return Container + */ + public function getContainer() { return $this->container; } - public function lock(): void + /** + * lock the container + */ + public function lock() { $this->container->lock(true); $this->dispatch(Events::MABS_ON_LOCKED); } - public function getAdapters(): array + /** + * list of active components + * @return array + */ + public function getAdapters() { - return [new SessionServiceAdapter()]; + return array( + new \Mabs\Adapter\SessionServiceAdapter(), + ); } - protected function load(): void + /** + * load all component in the DI Container + */ + protected function load() { - $this->container['event_dispatcher'] = fn(Container $container) => new EventDispatcher($container); - $this->container['request'] = fn() => Request::createFromGlobals(); - $this->container['router'] = fn(Container $container) => new Router(); + $this->container['event_dispatcher'] = function (Container $container) { + return new EventDispatcher($container); + }; + + $this->container['request'] = function () { + return Request::createFromGlobals(); + }; + + $this->container['router'] = function (Container $container) { + return new Router(); + }; foreach ($this->getAdapters() as $adapter) { $adapter->load($this->container); @@ -194,12 +299,14 @@ protected function load(): void $this->loaded = true; } - protected function boot(): void + /** + * initialize all components + */ + protected function boot() { foreach ($this->adapters as $adapter) { $adapter->boot($this->container); } - $this->dispatch(Events::MABS_ON_BOOT); $this->booted = true; } diff --git a/src/Container/Container.php b/src/Container/Container.php index 91c1560..24a5d46 100644 --- a/src/Container/Container.php +++ b/src/Container/Container.php @@ -29,165 +29,100 @@ * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ -declare(strict_types=1); - namespace Mabs\Container; -use Closure; -use ArrayAccess; -use InvalidArgumentException; -use LogicException; -use Psr\Container\ContainerInterface; -final class Container implements ContainerInterface, ArrayAccess +class Container implements \ArrayAccess { - private bool $isLocked = false; - /** @var array */ - private array $entries = []; + private $isLocked = false; - /** - * @param array $services Initial services. - */ - public function __construct(array $services = []) + protected $bag = array(); + + public function __construct($values = array()) { - foreach ($services as $key => $value) { - $this->set($key, $value); + foreach ($values as $key => $value) { + $this->offsetSet($key, $value); } } /** - * Retrieve a service by ID. + * @param mixed $offset + * An offset to check for. * - * @param string $id - * @return mixed - * @throws InvalidArgumentException if service is not defined. + * @return boolean true on success or false on failure. */ - public function get(string $id): mixed + public function offsetExists($offset) : bool { - if (!array_key_exists($id, $this->entries)) { - throw new InvalidArgumentException("Service \"$id\" not found."); - } - - $value = $this->entries[$id]; - - if ($value instanceof Closure) { - $this->entries[$id] = $value = $value($this); - } - - return $value; + return isset($this->bag[$offset]); } /** - * Check if a service exists. + * @param mixed $offset + * The offset to retrieve. * - * @param string $id - * @return bool + * @return mixed Can return all value types. */ - public function has(string $id): bool + public function offsetGet($offset) : mixed { - return array_key_exists($id, $this->entries); + if (!isset($this->bag[$offset])) { + throw new \InvalidArgumentException($offset.' not registred in container.'); + } + $value = $this->bag[$offset]; + if ($value instanceof \Closure) { + $this->bag[$offset] = $value($this); + } + + return $this->bag[$offset]; } /** - * Store a service in the container. - * - * @param string $id + * @param mixed $offset

+ * The offset to assign the value to. * @param mixed $value + * The value to set. + * * @return void - * @throws LogicException if the container is locked. */ - public function set(string $id, mixed $value): void + public function offsetSet($offset, $value) : void { - if ($this->isLocked && array_key_exists($id, $this->entries)) { - throw new LogicException("Cannot modify locked container entry \"$id\"."); + if ($this->isLocked && isset($this->bag[$offset])) { + throw new \LogicException('Cannot edit locked container '.$offset); } - - $this->entries[$id] = $value; + $this->bag[$offset] = $value; } /** - * Remove a service from the container. + * Offset to unset + * @param mixed $offset + * The offset to unset. * - * @param string $id * @return void - * @throws LogicException if the container is locked. */ - public function unset(string $id): void + public function offsetUnset($offset) : void { if ($this->isLocked) { - throw new LogicException("Cannot remove from a locked container."); + throw new \LogicException('Cannot edit locked container'); + } + if ($this->offsetExists($offset)) { + unset($this->bag[$offset]); } - - unset($this->entries[$id]); } /** - * Locks the container, preventing further modifications. + * lock Container */ - public function lock(): void + public function lock() { $this->isLocked = true; } /** - * Check if the container is locked. - */ - public function isLocked(): bool - { - return $this->isLocked; - } - - /** - * Create a new container with optional services and locking. - */ - public static function create(array $services = [], bool $lock = false): self - { - $container = new self($services); - if ($lock) { - $container->lock(); - } - return $container; - } - - /** - * Clone the container with new services. - * - * @param array $services - * @return self + * check if Container is locked + * @return bool */ - public function with(array $services): self - { - $clone = clone $this; - $clone->isLocked = false; - $clone->entries = [...$this->entries]; - - foreach ($services as $key => $value) { - $clone->set($key, $value); - } - - return $clone; - } - - // --- ArrayAccess Implementation --- - - public function offsetExists(mixed $offset): bool - { - return $this->has((string) $offset); - } - - public function offsetGet(mixed $offset): mixed - { - return $this->get((string) $offset); - } - - public function offsetSet(mixed $offset, mixed $value): void - { - $this->set((string) $offset, $value); - } - - public function offsetUnset(mixed $offset): void + public function isLocked() { - $this->unset((string) $offset); + return $this->isLocked === true; } } diff --git a/src/Container/ContainerNotFoundException.php b/src/Container/ContainerNotFoundException.php deleted file mode 100644 index d101a6d..0000000 --- a/src/Container/ContainerNotFoundException.php +++ /dev/null @@ -1,38 +0,0 @@ - - * @copyright 2015 Mohamed Aymen Ben Slimane - * - * The MIT License (MIT) - * - * Copyright (c) 2015 Mohamed Aymen Ben Slimane - * - * Permission is hereby granted, free of charge, to any person obtaining - * a copy of this software and associated documentation files (the - * "Software"), to deal in the Software without restriction, including - * without limitation the rights to use, copy, modify, merge, publish, - * distribute, sublicense, and/or sell copies of the Software, and to - * permit persons to whom the Software is furnished to do so, subject to - * the following conditions: - * - * The above copyright notice and this permission notice shall be - * included in all copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, - * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF - * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND - * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE - * LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION - * OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION - * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. - */ - - declare(strict_types=1); - - namespace Mabs\Container; - - use Psr\Container\NotFoundExceptionInterface; - -final class ContainerNotFoundException extends \Exception implements NotFoundExceptionInterface {} \ No newline at end of file diff --git a/src/Dispatcher/EventDispatcher.php b/src/Dispatcher/EventDispatcher.php index 8a89b17..f8bbf74 100644 --- a/src/Dispatcher/EventDispatcher.php +++ b/src/Dispatcher/EventDispatcher.php @@ -29,114 +29,109 @@ * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ - declare(strict_types=1); - - namespace Mabs\Dispatcher; - - use Closure; - use Psr\Container\ContainerInterface; - - final class EventDispatcher implements EventDispatcherInterface - { - /** @var array> */ - private array $listeners = []; - - public function __construct( - private readonly ContainerInterface $container - ) {} - - /** - * Dispatch an event to all registered listeners - * - * @param string $eventName The event to dispatch - * @param mixed $data Optional data to pass to the event listeners - * @return $this - */ - public function dispatch(string $eventName, mixed $data = null): self - { - foreach ($this->getListenersByEvent($eventName) as $event) { - $event['callback']($this->container, $data); - } - - return $this; - } - - /** - * Remove all listeners for a given event - * - * @param string $eventName The event name to clear - * @return $this - */ - final public function detach(string $eventName): self - { - unset($this->listeners[$eventName]); - - return $this; - } - - /** - * Register an event listener - * - * @param string $eventName The event to listen for - * @param callable|array|string $callback The callback to execute - * @param int $priority The priority (higher numbers execute first) - * @return $this - */ - final public function register( - string $eventName, - callable|array|string $callback, - int $priority = 0 - ): self { - $eventName = trim($eventName); - $this->listeners[$eventName] ??= []; - - $this->listeners[$eventName][] = [ - 'eventName' => $eventName, - 'callback' => $this->normalizeCallback($callback), - 'priority' => $priority - ]; - - if (count($this->listeners[$eventName]) > 1) { - usort( - $this->listeners[$eventName], - fn(array $a, array $b): int => $b['priority'] <=> $a['priority'] - ); - } - - return $this; - } - - /** - * Get all registered listeners - * - * @return array> - */ - public function getListeners(): array - { - return $this->listeners; - } - - /** - * Get listeners for a specific event - * - * @param string $eventName The event name - * @return array - */ - public function getListenersByEvent(string $eventName): array - { - return $this->listeners[$eventName] ?? []; - } - - /** - * Normalize different callback formats to a callable - */ - private function normalizeCallback(callable|array|string $callback): callable - { - return match (true) { - is_callable($callback) => $callback, - is_string($callback) && str_contains($callback, '::') => explode('::', $callback), - is_string($callback) => fn(ContainerInterface $c) => $c->get($callback), - default => throw new \InvalidArgumentException('Invalid callback type') - }; - } - } \ No newline at end of file +namespace Mabs\Dispatcher; + + +class EventDispatcher +{ + + private $listeners = array(); + + private $container; + + public function __construct($container) + { + $this->container = $container; + } + /** + * @param string $eventName + * @param mixed $data + * @return EventDispatcher + */ + public function dispatch($eventName, $data = null) + { + $listeners = $this->getListenersByEvent($eventName); + if (empty($listeners)) { + return $this; + } + foreach ($listeners as $event) { + call_user_func_array($event['callback'], array($this->container, $data)); + } + + return $this; + } + + /** + * + * @param string $eventName + * @return EventDispatcher + */ + final public function detach($eventName) + { + if (isset($this->listeners[$eventName])) { + unset($this->listeners[$eventName]); + } + + return $this; + } + + /** + * + * @param string $eventName + * @param mixed $callback + * @param int $priority + * @return EventDispatcher + */ + final public function register($eventName, $callback, $priority) + { + $eventName = trim($eventName); + + if (!isset($this->listeners[$eventName])) { + $this->listeners[$eventName] = array(); + } + + $event = array( + 'eventName' => $eventName, + 'callback' => $callback, + 'priority' => (int)$priority + ); + + array_push($this->listeners[$eventName], $event); + + if (count($this->listeners[$eventName]) > 1) { + usort($this->listeners[$eventName], function ($a, $b) + { + if ($a['priority'] == $b['priority']) { + return 0; + } + + return ($a['priority'] < $b['priority']) ? -1 : 1; + } + ); + } + + return $this; + } + + /** + * + * @return array + */ + public function getListeners() + { + return $this->listeners; + } + + /** + * @param $eventName + * @return array + */ + public function getListenersByEvent($eventName) + { + if (isset($this->listeners[$eventName])) { + return $this->listeners[$eventName]; + } + + return array(); + } +} diff --git a/src/Dispatcher/EventDispatcherInterface.php b/src/Dispatcher/EventDispatcherInterface.php deleted file mode 100644 index f785843..0000000 --- a/src/Dispatcher/EventDispatcherInterface.php +++ /dev/null @@ -1,38 +0,0 @@ - - * @copyright 2015 Mohamed Aymen Ben Slimane - * - * The MIT License (MIT) - * - * Copyright (c) 2015 Mohamed Aymen Ben Slimane - * - * Permission is hereby granted, free of charge, to any person obtaining - * a copy of this software and associated documentation files (the - * "Software"), to deal in the Software without restriction, including - * without limitation the rights to use, copy, modify, merge, publish, - * distribute, sublicense, and/or sell copies of the Software, and to - * permit persons to whom the Software is furnished to do so, subject to - * the following conditions: - * - * The above copyright notice and this permission notice shall be - * included in all copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, - * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF - * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND - * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE - * LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION - * OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION - * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. - */ - -namespace Mabs\Dispatcher; - -interface EventDispatcherInterface -{ - public function register(string $eventName, callable $callback, int $priority = 0); - public function dispatch(string $eventName, $data = null); -} diff --git a/src/Events.php b/src/Events.php index d89e0fe..905ef85 100644 --- a/src/Events.php +++ b/src/Events.php @@ -31,16 +31,15 @@ namespace Mabs; + class Events { - public const string MABS_BEFORE_LOAD = 'mabs.before.load'; - public const string MABS_ON_LOCKED = 'mabs.on.locked'; - public const string MABS_ON_BOOT = 'mabs.on.boot'; - public const string MABS_HANDLE_EXCEPTION = 'mabs.handle.exception'; - public const string MABS_HANDLE_REQUEST = 'mabs.handle.request'; - public const string MABS_ON_TERMINATE = 'mabs.on.terminate'; - public const string MABS_ON_FINISH = 'mabs.on.finish'; - - private function __construct() - {} + const MABS_BEFORE_LOAD = 'mabs.before.load'; + const MABS_ON_LOCKED = 'mabs.on.locked'; + const MABS_ON_BOOT = 'mabs.on.boot'; + const MABS_HANDLE_EXCEPTION = 'mabs.handle.exception'; + const MABS_HANDLE_REQUEST = 'mabs.handle.request'; + const MABS_ON_TERMINATE = 'mabs.on.terminate'; + const MABS_ON_FINISH = 'mabs.on.finish'; } + \ No newline at end of file diff --git a/src/Router/Route.php b/src/Router/Route.php index c3a70b1..0307718 100644 --- a/src/Router/Route.php +++ b/src/Router/Route.php @@ -29,65 +29,95 @@ * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ -declare(strict_types=1); - namespace Mabs\Router; -final class Route + +class Route { - private string $path; - private mixed $handler; - private ?string $name = null; + private $path; + private $callback; + private $name; - public function __construct(string $path, mixed $handler) + /** + * @return mixed + */ + public function getPath() { - $this->path = $path; - $this->handler = $handler; + return $this->path; } - public function path(): string + /** + * @param mixed $path + */ + public function setPath($path) { - return $this->path; + $this->path = $path; + + return $this; } - public function name(): string + /** + * @return mixed + */ + public function getName() { - return $this->name ??= spl_object_hash($this); + if ($this->name == null) { + $this->name = spl_object_hash($this); + } + + return $this->name; } - public function withName(string $name): self + /** + * @param mixed $name + */ + public function setName($name) { $this->name = $name; + return $this; } - public function handler(): mixed + /** + * @return mixed + */ + public function getCallback() { - return $this->handler; + return $this->callback; } - public function withHandler(mixed $handler): self + /** + * @param mixed $callback + */ + public function setCallback($callback) { - $this->handler = $handler; + $this->callback = $callback; + return $this; } - public function toRegex(): string + public function getRegularExpression() { - return preg_replace( - ['/\{(\w+)\?\}/', '/\{(\w+)\}/'], - ['(\w*)', '(\w+)'], - $this->path - ) ?? $this->path; + $path = $this->path; + $patterns = array('/\((\w+)\?\)/', '/\((\w+)\)/'); + $replacements = array('(\w*)', '(\w+)'); + + return preg_replace($patterns, $replacements, $path); } - public function extractParameters(array $matches): array + public function getNamesParameters(array $matchedValues = array()) { - preg_match_all('/\{(\w+)\??\}/', $this->path, $paramNames); + $path = $this->path; + $pattern = '/\((\w+)\??\)/'; + + preg_match_all($pattern, $path, $matches); + $params = array(); + if (isset($matches[1]) && is_array($matches[1])) { + foreach ($matches[1] as $index => $matche) { + $params[$matche] = isset($matchedValues[$index + 1]) ? $matchedValues[$index + 1] : null; + } + } - return array_combine( - $paramNames[1], - array_slice($matches, 1, count($paramNames[1])) + array_fill(0, count($paramNames[1]), null) - ) ?: []; + return $params; } } diff --git a/src/Router/Router.php b/src/Router/Router.php index 43246ae..5214c2a 100644 --- a/src/Router/Router.php +++ b/src/Router/Router.php @@ -29,21 +29,21 @@ * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ -declare(strict_types=1); - namespace Mabs\Router; use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpFoundation\Response; -final class Router +class Router { - private array $routeCollection = []; - private array $routes = []; + protected $routeCollection = array(); - public const HTTP_METHODS = [ - Request::METHOD_GET, + protected $routes = array(); + + public static $httpMethodes = array( + Request::METHOD_POST, Request::METHOD_HEAD, + Request::METHOD_GET, Request::METHOD_POST, Request::METHOD_PUT, Request::METHOD_PATCH, @@ -52,87 +52,114 @@ final class Router Request::METHOD_OPTIONS, Request::METHOD_TRACE, Request::METHOD_CONNECT, - ]; - - public function mount(Route $route, array $methods = []): self + ); + + /** + * mount controller for given route + * @param $route + * @param array $methodes + * @return \Mabs\Router + */ + public function mount($route, $methodes = array()) { - $methods = $methods ?: self::HTTP_METHODS; - $this->routes[$route->name()] = $route; - - foreach ($methods as $method) { - $this->routeCollection[$method][$route->name()] = $route; + if (empty($methodes)) { + $methodes = self::$httpMethodes; + } + $this->routes[$route->getName()] = $route; + foreach ($methodes as $methode) { + if (!isset($this->routeCollection[$methode])) { + $this->routeCollection[$methode] = array(); + } + $this->routeCollection[$methode][$route->getName()] = $route; } return $this; } - public function handle(Request $request): Response + /** + * handle Request and get the response + * @param Request $request + * @return mixed|Response + */ + public function handleRequest(Request $request) { - $method = $request->getMethod(); + $methode = $request->getMethod(); - if (!isset($this->routeCollection[$method])) { + if (!isset($this->routeCollection[$methode])) { return new Response('404 Not Found', 404); } + $routes = $this->routeCollection[$methode]; + foreach ($routes as $route) { - foreach ($this->routeCollection[$method] as $route) { if ($this->match($request, $route)) { - return $this->execute($route->handler(), $request); + + if (isset($routes[$route->getName()])) { + return $this->executeController($routes[$route->getName()]->getCallback(), $request); + } } } return new Response('404 Not Found', 404); } - public function generateUrl(string $routeName, array $params = []): string + /** + * generate Url for the given route name + * @param $routeName + * @param array $params + * @return mixed + */ + public function generateUrl($routeName, $params = array()) { $route = $this->getRouteByName($routeName); - $path = $route->path(); + $path = $route->getPath(); foreach ($params as $key => $value) { - $path = str_replace(['{' . $key . '}', '{' . $key . '?}'], $value, $path); + $path = str_replace(array('(' . $key . ')', '(' . $key . '?)'), $value, $path); } return $path; } - private function match(Request $request, Route $route): bool + protected function executeController($controller, Request $request) { - $currentPath = $this->normalizePath($request->getPathInfo()); - $routePath = $route->path(); - $regex = $route->toRegex(); + return call_user_func_array($controller, $request->query->all()); + } + + protected function match(Request $request, Route $route) + { + $currentPath = $this->getCurrentPath($request); + $routePath = $route->getPath(); + + $regex = $route->getRegularExpression(); + + if ($currentPath == $routePath) { - if ($currentPath === $routePath) { return true; - } + } else if (!empty($regex) && preg_match('#^' . $regex . '\/?$#', $currentPath, $matches)) { + $request->query->add($route->getNamesParameters($matches)); - if (!empty($regex) && preg_match('#^' . $regex . '/?$#', $currentPath, $matches)) { - $params = $route->extractParameters($matches); - $request->query->add($params); return true; } return false; } - private function execute(mixed $handler, Request $request): Response + private function getRouteByName($routeName) { - $result = is_callable($handler) - ? call_user_func_array($handler, $request->query->all()) - : $handler; + if (isset($this->routes[$routeName])) { + return $this->routes[$routeName]; + } - return $result instanceof Response - ? $result - : new Response((string) $result); + throw new \RuntimeException('route ' . $routeName . ' not found'); } - private function getRouteByName(string $routeName): Route + private function getCurrentPath(Request $request) { - return $this->routes[$routeName] ?? throw new \RuntimeException("Route '{$routeName}' not found."); - } + $currentPath = ltrim($request->getPathInfo(), '/'); + if (empty($currentPath) || $currentPath[strlen($currentPath) - 1] != '/') { + $currentPath .= '/'; + } - private function normalizePath(string $path): string - { - $normalized = ltrim($path, '/'); - return str_ends_with($normalized, '/') ? $normalized : $normalized . '/'; + return $currentPath; } } diff --git a/src/ServiceAdapterInterface.php b/src/ServiceAdapterInterface.php index 4136816..06762ba 100644 --- a/src/ServiceAdapterInterface.php +++ b/src/ServiceAdapterInterface.php @@ -31,17 +31,11 @@ namespace Mabs; -use Psr\Container\ContainerInterface; +use Mabs\Container\Container; interface ServiceAdapterInterface { - /** - * Register services into the container. - */ - public function load(ContainerInterface $container): void; - - /** - * Boot services after registration. - */ - public function boot(ContainerInterface $container): void; + public function load(Container $container); + public function boot(Container $container); } + \ No newline at end of file diff --git a/src/autoload.php b/src/autoload.php new file mode 100644 index 0000000..4981f4d --- /dev/null +++ b/src/autoload.php @@ -0,0 +1,32 @@ +container = new Container(); - $this->adapter = new SessionServiceAdapter(); - - // Mock d'un EventDispatcher - $eventDispatcherMock = $this->createMock(\Mabs\Dispatcher\EventDispatcherInterface::class); - $this->container['event_dispatcher'] = $eventDispatcherMock; - } - - public function testLoadInitializesSessionServices(): void - { - // Charge les services de session dans le container - $this->adapter->load($this->container); - - // Vérifie que les services de session existent dans le container - $this->assertTrue(isset($this->container['session.storage.handler'])); - $this->assertTrue(isset($this->container['session.storage.native'])); - $this->assertTrue(isset($this->container['session'])); - } - - public function testSessionHandlerIsNativeFileSessionHandler(): void - { - // Charge les services de session - $this->adapter->load($this->container); - - // Vérifie que le handler de session est bien de type NativeFileSessionHandler - $handler = $this->container['session.storage.handler']; - $this->assertInstanceOf(NativeFileSessionHandler::class, $handler); - } - - public function testSessionStorageIsNativeSessionStorage(): void - { - // Charge les services de session - $this->adapter->load($this->container); - - // Vérifie que le stockage de session est bien de type NativeSessionStorage - $storage = $this->container['session.storage.native']; - $this->assertInstanceOf(NativeSessionStorage::class, $storage); - } - - public function testSessionIsCreated(): void - { - // Charge les services de session - $this->adapter->load($this->container); - - // Vérifie que la session est bien créée - $session = $this->container['session']; - $this->assertInstanceOf(Session::class, $session); - } - - public function testBootSetsSessionInRequest(): void - { - // Charge les services de session - $this->adapter->load($this->container); - - // Crée une fausse requête - $request = $this->createMock(Request::class); - - // Attache la session à la requête via le boot - $this->container['request'] = $request; - $this->adapter->boot($this->container); - - // Vérifie que la session est bien assignée à la requête - $request->expects($this->once()) - ->method('setSession') - ->with($this->container['session']); - } - - public function testOnMabsBootSetsSessionInRequest(): void - { - // Charge les services de session - $this->adapter->load($this->container); - - // Crée une fausse requête - $request = $this->createMock(Request::class); - - // Crée l'événement MABS_ON_BOOT - $this->container['request'] = $request; - $this->adapter->onMabsBoot($this->container); - - // Vérifie que la session est bien assignée - $request->expects($this->once()) - ->method('setSession') - ->with($this->container['session']); - } - - public function testBootEventRegistration(): void - { - // Test que l'événement est bien enregistré - $this->container['event_dispatcher']->expects($this->once()) - ->method('register') - ->with( - Events::MABS_ON_BOOT, - [$this->adapter, 'onMabsBoot'], - 128 - ); - - $this->adapter->boot($this->container); - } -} diff --git a/tests/ApplicationTest.php b/tests/ApplicationTest.php index df05cc0..ddee4d9 100644 --- a/tests/ApplicationTest.php +++ b/tests/ApplicationTest.php @@ -1,75 +1,84 @@ app = new Application(debug: true); + $this->app = new Application(true); // Activer le mode debug pour les tests } - public function testAppStartsInDebugMode(): void + public function testConstructorSetsDebugMode() { - $this->assertTrue($this->app->isDebugMode()); + $app = new Application(true); + $this->assertTrue($app->isDebugMode()); + + $app = new Application(false); + $this->assertFalse($app->isDebugMode()); } - public function testContainerIsInstanceOfContainer(): void + public function testLoadInitializesComponents() { $this->assertInstanceOf(Container::class, $this->app->getContainer()); } - public function testApplicationHandlesRequestAndReturnsResponse(): void + public function testRunHandlesRequestSuccessfully() { - // Mock Router - $mockRouter = $this->createMock(Router::class); - $mockResponse = new Response('Hello World', 200); - $mockRouter->method('handleRequest')->willReturn($mockResponse); - $this->app->getContainer()['router'] = fn() => $mockRouter; + $request = Request::create('/test'); - $response = $this->app->handleRequest(); + $response = $this->app->handleRequest($request); $this->assertInstanceOf(Response::class, $response); - $this->assertSame(200, $response->getStatusCode()); - $this->assertSame('Hello World', $response->getContent()); } - public function testEventDispatching(): void + public function testGetMethodMountsRoute() + { + $this->app->get('example', function () { + return new Response('Hello, World!'); + }); + + $request = Request::create('/example'); + $response = $this->app->handleRequest($request); + + $this->assertEquals('Hello, World!', $response->getContent()); + } + + public function testEventDispatcher() { - $mockDispatcher = $this->createMock(EventDispatcher::class); - $mockDispatcher->expects($this->once()) - ->method('dispatch') - ->with('custom_event', ['key' => 'value']) - ->willReturn('dispatched'); + $eventCalled = false; + + $this->app->on('test.event', function () use (&$eventCalled) { + $eventCalled = true; + }); - $this->app->getContainer()['event_dispatcher'] = fn() => $mockDispatcher; + $this->app->dispatch('test.event'); - $result = $this->app->dispatch('custom_event', ['key' => 'value']); - $this->assertSame('dispatched', $result); + $this->assertTrue($eventCalled); } - public function testRouteMounting(): void + public function testDetachRemovesEventListener() { - $mockRouter = $this->createMock(Router::class); - $mockRouter->expects($this->once()) - ->method('mount') - ->with( - $this->isInstanceOf(\Mabs\Router\Route::class), - [Request::METHOD_GET] - ); - - $this->app->getContainer()['router'] = fn() => $mockRouter; - $this->app->get('/test', fn() => 'ok'); + $eventCalled = false; + + $callback = function () use (&$eventCalled) { + $eventCalled = true; + }; + + $this->app->on('test.event', $callback); + $this->app->detach('test.event'); + + $this->app->dispatch('test.event'); + + $this->assertFalse($eventCalled); } }