diff --git a/.github/workflows/cs-tests.yml b/.github/workflows/cs-tests.yml new file mode 100644 index 0000000..3da9965 --- /dev/null +++ b/.github/workflows/cs-tests.yml @@ -0,0 +1,46 @@ +on: + - push + +name: Run phpcs checks + +jobs: + mutation: + name: PHP ${{ matrix.php }}-${{ matrix.os }} + + runs-on: ${{ matrix.os }} + + strategy: + matrix: + os: + - ubuntu-latest + + php: + - "8.1" + - "8.2" + + steps: + - name: Checkout + uses: actions/checkout@v3 + + - name: Install PHP + uses: shivammathur/setup-php@v2 + with: + php-version: "${{ matrix.php }}" + tools: composer:v2, cs2pr + coverage: none + + - name: Determine composer cache directory + run: echo "COMPOSER_CACHE_DIR=$(composer config cache-dir)" >> $GITHUB_ENV + + - name: Cache dependencies installed with composer + uses: actions/cache@v3 + with: + path: ${{ env.COMPOSER_CACHE_DIR }} + key: php${{ matrix.php }}-composer-${{ hashFiles('**/composer.json') }} + restore-keys: | + php${{ matrix.php }}-composer- + - name: Install dependencies with composer + run: composer update --prefer-dist --no-interaction --no-progress --optimize-autoloader --ansi + + - name: Run phpcs checks + run: vendor/bin/phpcs diff --git a/.github/workflows/static-analisys.yml b/.github/workflows/static-analisys.yml new file mode 100644 index 0000000..74550fc --- /dev/null +++ b/.github/workflows/static-analisys.yml @@ -0,0 +1,46 @@ +on: + - push + +name: Run static analysis + +jobs: + mutation: + name: PHP ${{ matrix.php }}-${{ matrix.os }} + + runs-on: ${{ matrix.os }} + + strategy: + matrix: + os: + - ubuntu-latest + + php: + - "8.1" + - "8.2" + + steps: + - name: Checkout + uses: actions/checkout@v3 + + - name: Install PHP + uses: shivammathur/setup-php@v2 + with: + php-version: "${{ matrix.php }}" + tools: composer:v2, cs2pr + coverage: none + + - name: Determine composer cache directory + run: echo "COMPOSER_CACHE_DIR=$(composer config cache-dir)" >> $GITHUB_ENV + + - name: Cache dependencies installed with composer + uses: actions/cache@v3 + with: + path: ${{ env.COMPOSER_CACHE_DIR }} + key: php${{ matrix.php }}-composer-${{ hashFiles('**/composer.json') }} + restore-keys: | + php${{ matrix.php }}-composer- + - name: Install dependencies with composer + run: composer update --prefer-dist --no-interaction --no-progress --optimize-autoloader --ansi + + - name: Run static analysis + run: vendor/bin/psalm --no-cache --output-format=github --show-info=false --threads=4 diff --git a/.github/workflows/unit-tests.yml b/.github/workflows/unit-tests.yml new file mode 100644 index 0000000..593bed9 --- /dev/null +++ b/.github/workflows/unit-tests.yml @@ -0,0 +1,46 @@ +on: + - push + +name: Run PHPUnit tests + +jobs: + mutation: + name: PHP ${{ matrix.php }}-${{ matrix.os }} + + runs-on: ${{ matrix.os }} + + strategy: + matrix: + os: + - ubuntu-latest + + php: + - "8.1" + - "8.2" + + steps: + - name: Checkout + uses: actions/checkout@v3 + + - name: Install PHP + uses: shivammathur/setup-php@v2 + with: + php-version: "${{ matrix.php }}" + tools: composer:v2, cs2pr + coverage: none + + - name: Determine composer cache directory + run: echo "COMPOSER_CACHE_DIR=$(composer config cache-dir)" >> $GITHUB_ENV + + - name: Cache dependencies installed with composer + uses: actions/cache@v3 + with: + path: ${{ env.COMPOSER_CACHE_DIR }} + key: php${{ matrix.php }}-composer-${{ hashFiles('**/composer.json') }} + restore-keys: | + php${{ matrix.php }}-composer- + - name: Install dependencies with composer + run: composer install --prefer-dist --no-interaction --no-progress --optimize-autoloader --ansi + + - name: Run PHPUnit tests + run: vendor/bin/phpunit --colors=always diff --git a/.gitignore b/.gitignore index f3e6777..734d003 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,8 @@ +clover.xml +coveralls-upload.json +.phpcs-cache +.phpunit.result.cache + # Created by .ignore support plugin (hsz.mobi) ### Composer template composer.phar diff --git a/README.md b/README.md index 5ae3707..9c263a7 100644 --- a/README.md +++ b/README.md @@ -1,25 +1,96 @@ # dot-session ![OSS Lifecycle](https://img.shields.io/osslifecycle/dotkernel/dot-session) -![PHP from Packagist (specify version)](https://img.shields.io/packagist/php-v/dotkernel/dot-session/5.2.0) +![PHP from Packagist (specify version)](https://img.shields.io/packagist/php-v/dotkernel/dot-session/5.4.0) [![GitHub issues](https://img.shields.io/github/issues/dotkernel/dot-session)](https://github.com/dotkernel/dot-session/issues) [![GitHub forks](https://img.shields.io/github/forks/dotkernel/dot-session)](https://github.com/dotkernel/dot-session/network) [![GitHub stars](https://img.shields.io/github/stars/dotkernel/dot-session)](https://github.com/dotkernel/dot-session/stargazers) [![GitHub license](https://img.shields.io/github/license/dotkernel/dot-session)](https://github.com/dotkernel/dot-session/blob/5.0/LICENSE.md) +[![Build Static](https://github.com/dotkernel/dot-session/actions/workflows/static-analysis.yml/badge.svg?branch=5.0)](https://github.com/dotkernel/dot-session/actions/workflows/static-analysis.yml) + +[![SymfonyInsight](https://insight.symfony.com/projects/f6038340-d76b-4da8-9016-0472d4899f0a/big.svg)](https://insight.symfony.com/projects/f6038340-d76b-4da8-9016-0472d4899f0a) + + DotKernel session component extending and customizing [laminas-session](https://github.com/laminas/laminas-session) ## Installation Run the following command in your project folder -```bash -$ composer require dotkernel/dot-session -``` + + composer require dotkernel/dot-session + + +## Configuration +Register `SessionMiddleware` in your application's pipeline by adding the following line to `config/pipeline.php`: + + $app->pipe(Dot\Session\SessionMiddleware::class); + + +Register `dot-session`'s ConfigProvider in your application's configurations by adding the following line to `config/config.php`: + + \Dot\Session\ConfigProvider::class, + ## Usage +Basic usage to access and use the session object in your services: + +### Method #1 - Factory +#### Step 1: Create a factory that retrieves the SessionManger from the container -Next, inject this service in you classes, wherever you need session. ```php -$app->pipe(SessionMiddleware::class); +class ExampleFactory +{ + // code + + public function __invoke(ContainerInterface $container) + { + return new ExampleService( + $container->get(SessionManager::class) + ) + } +} ``` + +Register the factory in any mode you register factories on your project. + +#### Step 2: Access through your Service + +```php + +class ExampleService +{ + private SessionManager $session; + + public function __construct(SessionManager $session) + { + $this->session = $session; + } + + //you methods +} +``` + +### Method 2 - Injection +If you use annotated injection you can inject the Session Manager in your services. + +```php +use Dot\AnnotatedServices\Annotation\Inject; +use Laminas\Session\SessionManager; + +class ExampleService +{ + private SessionManager $session; + + /** + * @Inject({SessionManager::class}) + */ + public function __construct(SessionManager $session) + { + $this->session = $session; + } + + //your methods +} +``` \ No newline at end of file diff --git a/composer.json b/composer.json index 0d3801a..034100e 100644 --- a/composer.json +++ b/composer.json @@ -20,16 +20,39 @@ "psr/http-message": "^1.0.1", "laminas/laminas-servicemanager": "^3.11.1", "laminas/laminas-session": "^2.12.1", - "psr/http-server-middleware": "^1.0.1" + "psr/http-server-middleware": "^1.0.1", + "laminas/laminas-stdlib": "^3.7.1" }, "require-dev": { - "phpunit/phpunit": "^9.5.20", - "squizlabs/php_codesniffer": "^3.6.2", - "laminas/laminas-stdlib": "^3.7.1" + "phpunit/phpunit": "^10.2", + "laminas/laminas-coding-standard": "^2.5", + "vimeo/psalm": "^5.13" }, "autoload": { "psr-4": { "Dot\\Session\\": "src/" } + }, + "autoload-dev": { + "psr-4": { + "DotTest\\Session\\": "test/" + } + }, + "scripts": { + "check": [ + "@cs-check", + "@test" + ], + "cs-check": "phpcs", + "cs-fix": "phpcbf", + "test": "phpunit --colors=always", + "test-coverage": "phpunit --colors=always --coverage-clover clover.xml", + "static-analysis": "psalm --shepherd --stats" + }, + "config": { + "sort-packages": true, + "allow-plugins": { + "dealerdirect/phpcodesniffer-composer-installer": true + } } } diff --git a/phpcs.xml b/phpcs.xml new file mode 100644 index 0000000..1efe663 --- /dev/null +++ b/phpcs.xml @@ -0,0 +1,20 @@ + + + + + + + + + + + + + + src + test + + + + diff --git a/phpunit.xml b/phpunit.xml new file mode 100644 index 0000000..4d86f46 --- /dev/null +++ b/phpunit.xml @@ -0,0 +1,12 @@ + + + + + ./test + + + diff --git a/psalm.xml b/psalm.xml new file mode 100644 index 0000000..7272b57 --- /dev/null +++ b/psalm.xml @@ -0,0 +1,17 @@ + + + + + + + + + diff --git a/src/ConfigProvider.php b/src/ConfigProvider.php index 8dad5d3..c46aa67 100644 --- a/src/ConfigProvider.php +++ b/src/ConfigProvider.php @@ -1,11 +1,6 @@ $this->getDependencyConfig(), - - 'session_manager' => [ + 'dependencies' => $this->getDependencyConfig(), + 'session_manager' => [ 'validators' => [], - 'options' => [], + 'options' => [], ], - - 'session_storage' => [ + 'session_storage' => [ 'type' => SessionArrayStorage::class, ], - 'session_containers' => [], ]; } - /** - * Merge our config with Laminas Session dependencies - * @return array - */ public function getDependencyConfig(): array { return [ - 'aliases' => [ + 'aliases' => [ SessionManager::class => ManagerInterface::class, ], - 'factories' => [ - ConfigInterface::class => SessionConfigFactory::class, - ManagerInterface::class => SessionManagerFactory::class, - StorageInterface::class => StorageFactory::class, - - SessionOptions::class => SessionOptionsFactory::class, + 'factories' => [ + ConfigInterface::class => SessionConfigFactory::class, + ManagerInterface::class => SessionManagerFactory::class, + StorageInterface::class => StorageFactory::class, + SessionOptions::class => SessionOptionsFactory::class, SessionMiddleware::class => SessionMiddlewareFactory::class, ], 'abstract_factories' => [ ContainerAbstractServiceFactory::class, - ] + ], ]; } } diff --git a/src/Exception/ExceptionInterface.php b/src/Exception/ExceptionInterface.php index 363d234..ac3feac 100644 --- a/src/Exception/ExceptionInterface.php +++ b/src/Exception/ExceptionInterface.php @@ -1,19 +1,9 @@ has(ManagerInterface::class) ? $container->get(ManagerInterface::class) : Container::getDefaultManager(); $options = $container->get(SessionOptions::class); + + if (! $options instanceof SessionOptions) { + throw new Exception('Unable to find ' . SessionOptions::class); + } + return new SessionMiddleware( $sessionManager, $options diff --git a/src/Factory/SessionOptionsFactory.php b/src/Factory/SessionOptionsFactory.php index 265a868..ceda6a8 100644 --- a/src/Factory/SessionOptionsFactory.php +++ b/src/Factory/SessionOptionsFactory.php @@ -1,29 +1,38 @@ get('config')['dot_session']); + if (! $container->has('config')) { + throw new Exception('Unable to find config'); + } + + $config = $container->get('config'); + + if (! isset($config['dot_session']) || ! is_array($config['dot_session'])) { + throw new Exception('Unable to find dot_session config'); + } + + $config = $config['dot_session']; + + return new SessionOptions($config); } } diff --git a/src/Options/SessionOptions.php b/src/Options/SessionOptions.php index c81a009..bbf18fe 100644 --- a/src/Options/SessionOptions.php +++ b/src/Options/SessionOptions.php @@ -1,37 +1,21 @@ rememberMeInactive; } - /** - * @param int $rememberMeInactive - */ - public function setRememberMeInactive(int $rememberMeInactive) + public function setRememberMeInactive(int $rememberMeInactive): void { $this->rememberMeInactive = $rememberMeInactive; } diff --git a/src/SessionMiddleware.php b/src/SessionMiddleware.php index ce9a319..1212d7e 100644 --- a/src/SessionMiddleware.php +++ b/src/SessionMiddleware.php @@ -1,65 +1,48 @@ defaultSessionManager = $sessionManager; - $this->options = $options; + $this->options = $options; Container::setDefaultManager($sessionManager); } - /** - * @param ServerRequestInterface $request - * @param RequestHandlerInterface $handler - * @return ResponseInterface - */ - public function process(ServerRequestInterface $request, RequestHandlerInterface $handler): ResponseInterface - { + public function process( + ServerRequestInterface $request, + RequestHandlerInterface $handler + ): ResponseInterface { $now = time(); - if (isset($this->defaultSessionManager->getStorage()['LAST_ACTIVITY']) - && $now - $this->defaultSessionManager->getStorage()['LAST_ACTIVITY'] > $this->options->getRememberMeInactive() + if ( + isset($this->defaultSessionManager->getStorage()['LAST_ACTIVITY']) + && + ($now - $this->defaultSessionManager->getStorage()['LAST_ACTIVITY']) + > $this->options->getRememberMeInactive() ) { $this->defaultSessionManager->destroy(['send_expire_cookie' => true, 'clear_storage' => true]); $this->defaultSessionManager->start(); } - $this->defaultSessionManager->getStorage()['LAST_ACTIVITY'] = $now; return $handler->handle($request); } } diff --git a/test/ConfigProviderTest.php b/test/ConfigProviderTest.php new file mode 100644 index 0000000..e0deeaf --- /dev/null +++ b/test/ConfigProviderTest.php @@ -0,0 +1,58 @@ +config = (new ConfigProvider())(); + } + + public function testHasDependencies(): void + { + $this->assertArrayHasKey('dependencies', $this->config); + } + + public function testDependenciesHasFactories(): void + { + $this->assertArrayHasKey('factories', $this->config['dependencies']); + $this->assertArrayHasKey(ConfigInterface::class, $this->config['dependencies']['factories']); + $this->assertArrayHasKey(ManagerInterface::class, $this->config['dependencies']['factories']); + $this->assertArrayHasKey(StorageInterface::class, $this->config['dependencies']['factories']); + $this->assertArrayHasKey(SessionOptions::class, $this->config['dependencies']['factories']); + $this->assertArrayHasKey(SessionMiddleware::class, $this->config['dependencies']['factories']); + } + + public function testDependenciesHasAliases(): void + { + $this->assertArrayHasKey('aliases', $this->config['dependencies']); + $this->assertArrayHasKey(SessionManager::class, $this->config['dependencies']['aliases']); + } + + public function testDependenciesHasAbstractFactories(): void + { + $this->assertArrayHasKey('abstract_factories', $this->config['dependencies']); + } + + public function testConfigHasSessionStorage(): void + { + $this->assertArrayHasKey('session_storage', $this->config); + $this->assertIsArray($this->config['session_storage']); + $this->assertArrayHasKey('type', $this->config['session_storage']); + $this->assertNotEmpty($this->config['session_storage']['type']); + } +} diff --git a/test/Factory/ContainerAbstractServiceFactoryTest.php b/test/Factory/ContainerAbstractServiceFactoryTest.php new file mode 100644 index 0000000..cb4a6ad --- /dev/null +++ b/test/Factory/ContainerAbstractServiceFactoryTest.php @@ -0,0 +1,68 @@ + [ + 'name' => 'test', + ], + ]; + + /** + * @throws Exception + * @throws \PHPUnit\Framework\MockObject\Exception + */ + protected function setUp(): void + { + $this->container = $this->createMock(ContainerInterface::class); + $this->factory = new ContainerAbstractServiceFactory(); + } + + public function testInstantiate(): void + { + $this->container->expects($this->once()) + ->method('has') + ->with(ManagerInterface::class) + ->willReturn(true); + + $this->container->expects($this->once()) + ->method('get') + ->with(ManagerInterface::class) + ->willReturnMap([ManagerInterface::class]); + + $factory = (new ContainerAbstractServiceFactory())($this->container, 'dot-session.test'); + $this->assertInstanceOf(Container::class, $factory); + } + + public function testCanCreate(): void + { + $this->container->expects($this->once()) + ->method('has') + ->with('config') + ->willReturn(true); + + $this->container->expects($this->once()) + ->method('get') + ->with('config') + ->willReturn($this->config); + + $this->assertFalse($this->factory->canCreate($this->container, 'dot-session.not_in_config_name')); + $this->assertTrue($this->factory->canCreate($this->container, 'dot-session.test')); + } +} diff --git a/test/Factory/SessionMiddlewareFactoryTest.php b/test/Factory/SessionMiddlewareFactoryTest.php new file mode 100644 index 0000000..38a8095 --- /dev/null +++ b/test/Factory/SessionMiddlewareFactoryTest.php @@ -0,0 +1,91 @@ +container = $this->createMock(ContainerInterface::class); + } + + /** + * @throws ContainerExceptionInterface + * @throws NotFoundExceptionInterface + */ + public function testWillInstantiateWithDefaultManager(): void + { + $this->container->expects($this->once())->method('has') + ->with(ManagerInterface::class) + ->willReturn(false); + + $this->container->expects($this->once())->method('get') + ->with(SessionOptions::class) + ->willReturn(new SessionOptions()); + + $factory = (new SessionMiddlewareFactory())($this->container); + + $this->assertInstanceOf(SessionMiddleware::class, $factory); + } + + /** + * @throws ContainerExceptionInterface + * @throws NotFoundExceptionInterface + * @throws \PHPUnit\Framework\MockObject\Exception + */ + public function testWillInstantiateWithManagerInterface(): void + { + $managerInterface = $this->createMock(SessionManager::class); + $this->container->expects($this->once())->method('has') + ->with(ManagerInterface::class) + ->willReturn(true); + + $this->container->method('get') + ->willReturnMap([ + [ManagerInterface::class, $managerInterface], + [SessionOptions::class, new SessionOptions()], + ]); + + $factory = (new SessionMiddlewareFactory())($this->container); + + $this->assertInstanceOf(SessionMiddleware::class, $factory); + } + + /** + * @throws ContainerExceptionInterface + * @throws NotFoundExceptionInterface + */ + public function testWillReturnException(): void + { + $this->container->expects($this->once())->method('has') + ->with(ManagerInterface::class) + ->willReturn(false); + + $this->container->expects($this->once())->method('get') + ->with(SessionOptions::class) + ->willReturn(null); + + $this->expectException(Exception::class); + $this->expectExceptionMessage('Unable to find ' . SessionOptions::class); + (new SessionMiddlewareFactory())($this->container); + } +} diff --git a/test/Factory/SessionOptionsFactoryTest.php b/test/Factory/SessionOptionsFactoryTest.php new file mode 100644 index 0000000..501c282 --- /dev/null +++ b/test/Factory/SessionOptionsFactoryTest.php @@ -0,0 +1,81 @@ +container = $this->createMock(ContainerInterface::class); + } + + /** + * @throws ContainerExceptionInterface + * @throws NotFoundExceptionInterface + */ + public function testWillNotInstantiateWithoutConfig(): void + { + $this->container->expects($this->once()) + ->method('has') + ->with('config') + ->willReturn(false); + + $this->expectExceptionMessage('Unable to find config'); + $this->expectException(Exception::class); + (new SessionOptionsFactory())($this->container); + } + + /** + * @throws ContainerExceptionInterface + * @throws NotFoundExceptionInterface + */ + public function testWillNotInstantiateWithoutDotLog(): void + { + $this->container->expects($this->once()) + ->method('has') + ->with('config') + ->willReturn(true); + + $this->container->method('get')->willReturnMap([]); + + $this->expectExceptionMessage('Unable to find dot_session config'); + $this->expectException(Exception::class); + (new SessionOptionsFactory())($this->container); + } + + /** + * @throws ContainerExceptionInterface + * @throws NotFoundExceptionInterface + */ + public function testWillInstantiate(): void + { + $this->container->expects($this->once()) + ->method('has') + ->with('config') + ->willReturn(true); + + $this->container->method('get')->willReturn([ + 'dot_session' => [], + ]); + + $factory = (new SessionOptionsFactory())($this->container); + + $this->assertInstanceOf(SessionOptions::class, $factory); + } +} diff --git a/test/Options/SessionOptionsTest.php b/test/Options/SessionOptionsTest.php new file mode 100644 index 0000000..9be7bda --- /dev/null +++ b/test/Options/SessionOptionsTest.php @@ -0,0 +1,24 @@ +assertIsInt($sessionOptions->getRememberMeInactive()); + } + + public function testSetRememberMeInactive(): void + { + $sessionOptions = new SessionOptions(); + $sessionOptions->setRememberMeInactive(100); + $this->assertEquals(100, $sessionOptions->getRememberMeInactive()); + } +} diff --git a/test/SessionMiddlewareTest.php b/test/SessionMiddlewareTest.php index 7dcbe09..30dba89 100644 --- a/test/SessionMiddlewareTest.php +++ b/test/SessionMiddlewareTest.php @@ -1,20 +1,62 @@ sessionManager = $this->createMock(SessionManager::class); + $this->sessionOptions = $this->createMock(SessionOptions::class); + + $this->sessionMiddleware = new SessionMiddleware($this->sessionManager, $this->sessionOptions); + } + + /** + * @throws Exception + */ + public function testProcess(): void + { + $requestInterface = $this->createMock(ServerRequestInterface::class); + $handlerInterface = $this->createMock(RequestHandlerInterface::class); + $process = $this->sessionMiddleware->process($requestInterface, $handlerInterface); + $this->assertInstanceOf(ResponseInterface::class, $process); + } + + /** + * @throws Exception + */ + public function testProcessWithLastActivity(): void + { + $this->sessionManager->method('getStorage') + ->willReturn(['LAST_ACTIVITY' => 1689849724]); + + $this->sessionOptions->method('getRememberMeInactive') + ->willReturn(600); + + $requestInterface = $this->createMock(ServerRequestInterface::class); + $handlerInterface = $this->createMock(RequestHandlerInterface::class); + $process = $this->sessionMiddleware->process($requestInterface, $handlerInterface); + $this->assertInstanceOf(ResponseInterface::class, $process); } }