├── .gitignore ├── .travis.yml ├── tests ├── Stubs │ ├── ErrorHandler.php │ └── ErrorServiceProvider.php ├── MethodInjectionTest.php ├── SettingsTest.php ├── SlimDefinitionAggregateBuilderTest.php └── AppTest.php ├── src ├── Lean │ ├── App.php │ ├── MethodInjection.php │ └── SlimDefinitionAggregateBuilder.php └── Slim │ └── Settings.php ├── .php_cs ├── phpunit.xml ├── composer.json └── README.md /.gitignore: -------------------------------------------------------------------------------- 1 | composer.lock 2 | vendor 3 | .php_cs.cache 4 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: php 2 | 3 | php: 4 | - 7.0 5 | - 7.1 6 | - 7.2 7 | - 7.3 8 | 9 | sudo: false 10 | 11 | before_script: 12 | - travis_retry composer self-update 13 | - travis_retry composer update ${COMPOSER_FLAGS} --no-interaction --prefer-source 14 | - mkdir -p build/logs 15 | 16 | script: 17 | - ./vendor/bin/phpunit --coverage-clover ./build/logs/clover.xml 18 | 19 | after_success: 20 | - php vendor/bin/php-coveralls -v 21 | -------------------------------------------------------------------------------- /tests/Stubs/ErrorHandler.php: -------------------------------------------------------------------------------- 1 | getBody()->write('Something went wrong'); 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /tests/Stubs/ErrorServiceProvider.php: -------------------------------------------------------------------------------- 1 | container->share('errorHandler', function () { 16 | return new ErrorHandler(); 17 | }); 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /src/Lean/App.php: -------------------------------------------------------------------------------- 1 | delegate(new Container(SlimDefinitionAggregateBuilder::build($container))); 15 | $container->delegate(new ReflectionContainer()); 16 | 17 | parent::__construct($container); 18 | } 19 | 20 | public function getContainer(): Container 21 | { 22 | return parent::getContainer(); 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /.php_cs: -------------------------------------------------------------------------------- 1 | in(['src', 'tests']); 8 | 9 | return Config::create() 10 | ->setRules([ 11 | '@Symfony' => true, 12 | 'ordered_imports' => true, 13 | 'phpdoc_align' => false, 14 | 'phpdoc_to_comment' => false, 15 | 'phpdoc_inline_tag' => false, 16 | 'yoda_style' => false, 17 | 'blank_line_before_statement' => false, 18 | 'phpdoc_separation' => false, 19 | 'concat_space' => [ 20 | 'spacing' => 'one', 21 | ], 22 | 'array_syntax' => [ 23 | 'syntax' => 'short', 24 | ], 25 | ]) 26 | ->setFinder($finder); 27 | -------------------------------------------------------------------------------- /phpunit.xml: -------------------------------------------------------------------------------- 1 | 2 | 12 | 13 | 14 | tests/ 15 | 16 | 17 | 18 | 19 | ./src 20 | 21 | 22 | 23 | 24 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "jenssegers/lean", 3 | "description": "Use the league/container with auto-wiring support as the core container in Slim 3", 4 | "keywords": [ 5 | "league", 6 | "slim", 7 | "container" 8 | ], 9 | "homepage": "https://github.com/jenssegers/lean", 10 | "license": "MIT", 11 | "authors": [ 12 | { 13 | "name": "Jens Segers", 14 | "homepage": "https://jenssegers.com" 15 | } 16 | ], 17 | "require": { 18 | "php": ">=7.0", 19 | "slim/slim": "^3.0", 20 | "league/container": "^2.0|^3.0" 21 | }, 22 | "require-dev": { 23 | "phpunit/phpunit": "^6.0|^7.0", 24 | "php-coveralls/php-coveralls": "^2.0" 25 | }, 26 | "autoload": { 27 | "psr-4": { 28 | "Jenssegers\\Lean\\": "src/Lean", 29 | "Slim\\": "src/Slim" 30 | } 31 | }, 32 | "autoload-dev": { 33 | "psr-4": { 34 | "Jenssegers\\Lean\\Tests\\": "tests" 35 | } 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /tests/MethodInjectionTest.php: -------------------------------------------------------------------------------- 1 | add(DateTime::class, $yesterday); 22 | 23 | $request = $this->createMock(Request::class); 24 | $response = $this->createMock(Response::class); 25 | 26 | $callable = function (DateTime $day, $foo) use ($yesterday) { 27 | $this->assertEquals($day, $yesterday); 28 | $this->assertEquals('bar', $foo); 29 | }; 30 | 31 | $strategy($callable, $request, $response, ['foo' => 'bar']); 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /src/Lean/MethodInjection.php: -------------------------------------------------------------------------------- 1 | reflectionContainer = new ReflectionContainer(); 21 | $this->reflectionContainer->setContainer($container); 22 | } 23 | 24 | public function __invoke( 25 | callable $callable, 26 | ServerRequestInterface $request, 27 | ResponseInterface $response, 28 | array $routeArguments 29 | ) { 30 | foreach ($routeArguments as $k => $v) { 31 | $request = $request->withAttribute($k, $v); 32 | } 33 | 34 | // Re-assign the request to the container with the route modifications. 35 | $this->reflectionContainer->getContainer()->share('request', $request); 36 | 37 | return $this->reflectionContainer->call($callable, $routeArguments); 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /src/Slim/Settings.php: -------------------------------------------------------------------------------- 1 | data; 12 | 13 | foreach (explode('.', $key) as $segment) { 14 | if (!isset($array[$segment])) { 15 | return $default; 16 | } 17 | if (!is_array($array)) { 18 | return $default; 19 | } 20 | 21 | $array = $array[$segment]; 22 | } 23 | 24 | return $array; 25 | } 26 | 27 | public function set($key, $value) 28 | { 29 | $array = &$this->data; 30 | $keyPath = explode('.', $key); 31 | $endKey = array_pop($keyPath); 32 | 33 | foreach ($keyPath as $segment) { 34 | if (!isset($array[$segment])) { 35 | $array[$segment] = []; 36 | } 37 | if (!is_array($array[$segment])) { 38 | throw new RuntimeException("The value at $segment of $key is not an array"); 39 | } 40 | 41 | $array = &$array[$segment]; 42 | } 43 | 44 | $array[$endKey] = $value; 45 | } 46 | 47 | public function has($key): bool 48 | { 49 | $array = $this->data; 50 | 51 | foreach (explode('.', $key) as $segment) { 52 | if (array_key_exists($segment, $array)) { 53 | $array = $array[$segment]; 54 | } else { 55 | return false; 56 | } 57 | } 58 | 59 | return true; 60 | } 61 | 62 | public function remove($key) 63 | { 64 | $array = &$this->data; 65 | $keyPath = explode('.', $key); 66 | $endKey = array_pop($keyPath); 67 | 68 | foreach ($keyPath as $segment) { 69 | if (!isset($array[$segment])) { 70 | return; 71 | } 72 | 73 | $array = &$array[$segment]; 74 | } 75 | 76 | unset($array[$endKey]); 77 | } 78 | } 79 | -------------------------------------------------------------------------------- /tests/SettingsTest.php: -------------------------------------------------------------------------------- 1 | [ 15 | 'bar' => 'baz', 16 | ], 17 | ]); 18 | 19 | $this->assertEquals('baz', $settings->get('foo.bar')); 20 | $this->assertEquals(null, $settings->get('bar')); 21 | $this->assertEquals(['bar' => 'baz'], $settings->get('foo')); 22 | } 23 | 24 | public function testSetDotNotation() 25 | { 26 | $settings = new Settings(); 27 | $settings->set('foo.bar', 'baz'); 28 | 29 | $this->assertEquals('baz', $settings->get('foo.bar')); 30 | $this->assertEquals(['bar' => 'baz'], $settings->get('foo')); 31 | } 32 | 33 | public function testGetWithDefaultValue() 34 | { 35 | $settings = new Settings(); 36 | 37 | $this->assertEquals('baz', $settings->get('foo.bar', 'baz')); 38 | } 39 | 40 | public function testHasDotNotation() 41 | { 42 | $settings = new Settings([ 43 | 'foo' => [ 44 | 'bar' => 'baz', 45 | ], 46 | ]); 47 | 48 | $this->assertTrue($settings->has('foo.bar')); 49 | $this->assertFalse($settings->has('bar')); 50 | } 51 | 52 | public function testSetWithIncompatibleTypes() 53 | { 54 | $settings = new Settings([ 55 | 'foo' => 'bar', 56 | ]); 57 | 58 | $this->expectException(RuntimeException::class); 59 | $settings->set('foo.bar', 'baz'); 60 | } 61 | 62 | public function testRemoveDotNotation() 63 | { 64 | $settings = new Settings([ 65 | 'foo' => [ 66 | 'bar' => 'baz', 67 | 'c' => 'd', 68 | ], 69 | 'a' => 'b', 70 | ]); 71 | 72 | $settings->remove('foo.bar'); 73 | $this->assertEquals(['c' => 'd'], $settings->get('foo')); 74 | 75 | $settings->remove('foo'); 76 | $this->assertEquals(['a' => 'b'], $settings->all()); 77 | } 78 | } 79 | -------------------------------------------------------------------------------- /tests/SlimDefinitionAggregateBuilderTest.php: -------------------------------------------------------------------------------- 1 | container = new Container(); 19 | $this->container->delegate(new Container(SlimDefinitionAggregateBuilder::build($this->container))); 20 | } 21 | 22 | public function provideRequiredServices() 23 | { 24 | return [ 25 | ['settings', \Slim\Settings::class], 26 | ['environment', \Slim\Http\Environment::class], 27 | ['request', \Slim\Http\Request::class], 28 | [\Slim\Http\Request::class, \Slim\Http\Request::class], 29 | ['response', \Slim\Http\Response::class], 30 | [\Slim\Http\Response::class, \Slim\Http\Response::class], 31 | ['router', \Slim\Router::class], 32 | ['foundHandler', \Slim\Interfaces\InvocationStrategyInterface::class], 33 | ['phpErrorHandler', \Slim\Handlers\PhpError::class], 34 | ['errorHandler', \Slim\Handlers\Error::class], 35 | ['notFoundHandler', \Slim\Handlers\NotFound::class], 36 | ['notAllowedHandler', \Slim\Handlers\NotAllowed::class], 37 | ['callableResolver', \Slim\CallableResolver::class], 38 | ]; 39 | } 40 | 41 | /** 42 | * @dataProvider provideRequiredServices 43 | */ 44 | public function testItRegistersSlimServices(string $containerKey, string $expectedClassName) 45 | { 46 | $this->assertTrue($this->container->has($containerKey)); 47 | $this->assertInstanceOf($expectedClassName, $this->container->get($containerKey)); 48 | } 49 | 50 | public function testHasSettings() 51 | { 52 | $this->assertTrue($this->container->has('settings')); 53 | $this->assertInstanceOf(\Slim\Collection::class, $this->container->get('settings')); 54 | $this->assertEquals(4096, $this->container->get('settings')->get('responseChunkSize')); 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /tests/AppTest.php: -------------------------------------------------------------------------------- 1 | assertTrue($container->has('request')); 18 | } 19 | 20 | public function testItConstructsWithoutAContainer() 21 | { 22 | $app = new App(); 23 | $container = $app->getContainer(); 24 | $this->assertTrue($container->has('request')); 25 | } 26 | 27 | public function testItSupportsOverwritingDefaultDefinitions() 28 | { 29 | $app = new App(); 30 | $container = $app->getContainer(); 31 | $this->assertTrue($container->has('errorHandler')); 32 | 33 | $container->share('errorHandler', function () { 34 | return new ErrorHandler(); 35 | }); 36 | 37 | $this->assertInstanceOf(ErrorHandler::class, $app->getContainer()->get('errorHandler')); 38 | $this->assertTrue($app->getContainer()->has('request')); 39 | } 40 | 41 | public function testItDoesntOverwriteAlreadyRegisteredDefinitions() 42 | { 43 | $container = new Container(); 44 | $this->assertFalse($container->has('errorHandler')); 45 | 46 | $container->share('errorHandler', function () { 47 | return new ErrorHandler(); 48 | }); 49 | 50 | $app = new App($container); 51 | $this->assertInstanceOf(ErrorHandler::class, $app->getContainer()->get('errorHandler')); 52 | $this->assertTrue($app->getContainer()->has('request')); 53 | } 54 | 55 | public function testItSupportsPreInjectedServiceProviders() 56 | { 57 | $container = new Container(); 58 | $container->addServiceProvider(ErrorServiceProvider::class); 59 | $app = new App($container); 60 | 61 | $this->assertInstanceOf(ErrorHandler::class, $app->getContainer()->get('errorHandler')); 62 | $this->assertTrue($app->getContainer()->has('request')); 63 | } 64 | 65 | public function testItSupportsPostInjectedServiceProviders() 66 | { 67 | $app = new App(); 68 | $app->getContainer()->addServiceProvider(ErrorServiceProvider::class); 69 | 70 | $this->assertInstanceOf(ErrorHandler::class, $app->getContainer()->get('errorHandler')); 71 | $this->assertTrue($app->getContainer()->has('request')); 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /src/Lean/SlimDefinitionAggregateBuilder.php: -------------------------------------------------------------------------------- 1 | 'settings', 32 | Request::class => 'request', 33 | RequestInterface::class => 'request', 34 | ServerRequestInterface::class => 'request', 35 | Response::class => 'response', 36 | ResponseInterface::class => 'response', 37 | Router::class => 'router', 38 | RouterInterface::class => 'router', 39 | ]; 40 | 41 | /** 42 | * @var array 43 | */ 44 | protected static $defaultSettings = [ 45 | 'httpVersion' => '1.1', 46 | 'responseChunkSize' => 4096, 47 | 'outputBuffering' => 'append', 48 | 'determineRouteBeforeAppMiddleware' => false, 49 | 'displayErrorDetails' => false, 50 | 'addContentLengthHeader' => true, 51 | 'routerCacheFile' => false, 52 | 'methodInjection' => true, 53 | ]; 54 | 55 | public static function build(ContainerInterface $container): DefinitionAggregateInterface 56 | { 57 | $aggregate = new DefinitionAggregate(); 58 | $aggregate->setContainer($container); 59 | 60 | $aggregate->add('settings', function () { 61 | return new Settings(self::$defaultSettings); 62 | }, true); 63 | 64 | $aggregate->add('environment', function () { 65 | return new Environment($_SERVER); 66 | }, true); 67 | 68 | $aggregate->add('request', function () use ($container) { 69 | return Request::createFromEnvironment($container->get('environment')); 70 | }, true); 71 | 72 | $aggregate->add('response', function () use ($container) { 73 | $headers = new Headers(['Content-Type' => 'text/html; charset=UTF-8']); 74 | $response = new Response(200, $headers); 75 | 76 | return $response->withProtocolVersion($container->get('settings')['httpVersion']); 77 | }, true); 78 | 79 | $aggregate->add('router', function () use ($container) { 80 | $routerCacheFile = false; 81 | if (isset($container->get('settings')['routerCacheFile'])) { 82 | $routerCacheFile = $container->get('settings')['routerCacheFile']; 83 | } 84 | 85 | $router = (new Router())->setCacheFile($routerCacheFile); 86 | if (method_exists($router, 'setContainer')) { 87 | $router->setContainer($container); 88 | } 89 | 90 | return $router; 91 | }, true); 92 | 93 | $aggregate->add('foundHandler', function () use ($container) { 94 | if ($container->get('settings')['methodInjection']) { 95 | return new MethodInjection($container); 96 | } 97 | 98 | return new RequestResponse(); 99 | }, true); 100 | 101 | $aggregate->add('phpErrorHandler', function () use ($container) { 102 | return new PhpError($container->get('settings')['displayErrorDetails']); 103 | }, true); 104 | 105 | $aggregate->add('errorHandler', function () use ($container) { 106 | return new Error($container->get('settings')['displayErrorDetails']); 107 | }, true); 108 | 109 | $aggregate->add('notFoundHandler', function () { 110 | return new NotFound(); 111 | }, true); 112 | 113 | $aggregate->add('notAllowedHandler', function () { 114 | return new NotAllowed(); 115 | }, true); 116 | 117 | $aggregate->add('callableResolver', function () use ($container) { 118 | return new CallableResolver($container); 119 | }, true); 120 | 121 | // Register aliases. 122 | foreach (self::$aliases as $alias => $definition) { 123 | $aggregate->add($alias, function () use ($container, $definition) { 124 | return $container->get($definition); 125 | }, true); 126 | } 127 | 128 | return $aggregate; 129 | } 130 | } 131 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | Lean 2 | ==== 3 | 4 | [![Latest Stable Version](http://img.shields.io/packagist/v/jenssegers/lean.svg)](https://packagist.org/packages/jenssegers/lean) [![Build Status](http://img.shields.io/travis/jenssegers/lean.svg)](https://travis-ci.org/jenssegers/lean) [![Coverage Status](http://img.shields.io/coveralls/jenssegers/lean.svg)](https://coveralls.io/r/jenssegers/lean) 5 | 6 | Lean allows you to use the [PHP League's Container](https://github.com/thephpleague/container) package with auto-wiring support as the core container in [Slim 3](https://github.com/slimphp/Slim). 7 | 8 | ## Install 9 | 10 | Via Composer 11 | 12 | ``` bash 13 | $ composer require jenssegers/lean 14 | ``` 15 | 16 | ## Usage 17 | 18 | The easiest way to start using Lean is simply creating a `Jenssegers\Lean\App` instance: 19 | 20 | ``` php 21 | require 'vendor/autoload.php'; 22 | 23 | $app = new \Jenssegers\Lean\App(); 24 | 25 | $app->get('/hello/{name}', function (Request $request, Response $response, string $name) { 26 | return $response->write('Hello, ' . $name); 27 | }); 28 | 29 | $app->run(); 30 | ``` 31 | 32 | Behind the scenes, a Slim application is bootstrapped by adding all of the required Slim components to League's container. 33 | 34 | ## Service Providers 35 | 36 | Service providers give the benefit of organising your container definitions along with an increase in performance for larger applications as definitions registered within a service provider are lazily registered at the point where a service is retrieved. 37 | 38 | To build a service provider it is as simple as extending the base service provider and defining what you would like to register. 39 | 40 | ```php 41 | use League\Container\ServiceProvider\AbstractServiceProvider; 42 | 43 | class SomeServiceProvider extends AbstractServiceProvider 44 | { 45 | /** 46 | * The provided array is a way to let the container 47 | * know that a service is provided by this service 48 | * provider. Every service that is registered via 49 | * this service provider must have an alias added 50 | * to this array or it will be ignored. 51 | */ 52 | protected $provides = [ 53 | SomeInterface::class, 54 | ]; 55 | 56 | /** 57 | * This is where the magic happens, within the method you can 58 | * access the container and register or retrieve anything 59 | * that you need to, but remember, every alias registered 60 | * within this method must be declared in the `$provides` array. 61 | */ 62 | public function register() 63 | { 64 | $this->getContainer() 65 | ->add(SomeInterface::class, SomeImplementation::class); 66 | } 67 | } 68 | ``` 69 | 70 | To register this service provider with the container simply pass an instance of your provider or a fully qualified class name to the League\Container\Container::addServiceProvider method. 71 | 72 | ```php 73 | $app = new \Jenssegers\Lean\App(); 74 | $app->getContainer()->addServiceProvider(\Acme\ServiceProvider\SomeServiceProvider::class); 75 | ``` 76 | 77 | Read more about service providers [here](https://container.thephpleague.com/3.x/service-providers/). 78 | 79 | ## Settings 80 | 81 | You can access Slim's internal configuration through the `settings` key on the container: 82 | 83 | ```php 84 | $app = new \Jenssegers\Lean\App(); 85 | 86 | $app->getContainer()->get('settings')['displayErrorDetails'] = true; 87 | ``` 88 | 89 | Alternatively, an alias is registered that allows a bit more fluent way of working with settings: 90 | 91 | ```php 92 | $app = new \Jenssegers\Lean\App(); 93 | 94 | $app->getContainer()->get(\Slim\Settings::class)->set('displayErrorDetails', true); 95 | ``` 96 | 97 | Read more about the available configuration options [here](https://www.slimframework.com/docs/v3/objects/application.html#slim-default-settings). 98 | 99 | # Route arguments 100 | 101 | By default, Lean will use method injection to pass arguments to your routes. This allows you to type-hint dependencies on method level (similar to the Laravel framework). 102 | 103 | Route arguments will be passed as individual arguments to your method: 104 | 105 | ```php 106 | $app->get('/books/{id}', function (Request $request, Response $response, string $id) { 107 | ... 108 | }); 109 | ``` 110 | 111 | They are also accessible through the `getAttribute` method. 112 | 113 | ```php 114 | $app->get('/books/{id}', function (Request $request, Response $response) { 115 | $id = $request->getAttribute('id'); 116 | .... 117 | }); 118 | ``` 119 | 120 | If you want to disable this behaviour and use the default Slim way of route arguments, you can disable this feature be setting `methodInjection` to `false`: 121 | 122 | ```php 123 | $app->getContainer()->get(\Slim\Settings::class)->set('methodInjection', false); 124 | ``` 125 | 126 | Read more about routes [here](http://www.slimframework.com/docs/v3/objects/router.html). 127 | 128 | ## Error Handlers 129 | 130 | By default, Lean uses Slim's error handlers. There are different ways to implement an error handler for Slim, read more about them [here](https://www.slimframework.com/docs/v3/handlers/error.html). 131 | 132 | Typically you would create a custom error handler class that looks like this: 133 | 134 | ```php 135 | class CustomErrorHandler 136 | { 137 | public function __invoke(ServerRequestInterface $request, Response $response, Throwable $exception) 138 | { 139 | return $response->withJson([ 140 | 'error' => 'Something went wrong', 141 | ], 500); 142 | } 143 | } 144 | ``` 145 | 146 | Then you overwrite the default handler by adding it to the container: 147 | 148 | ```php 149 | $app = new Jenssegers\Lean\App(); 150 | 151 | $app->getContainer()->share('errorHandler', function () { 152 | return new CustomErrorHandler(); 153 | }); 154 | ``` 155 | 156 | Ideally, you would put this code inside a service provider. Read more about service providers above. 157 | 158 | ## Testing 159 | 160 | ``` bash 161 | $ php ./vendor/bin/phpunit 162 | ``` 163 | 164 | ## License 165 | 166 | The MIT License (MIT). 167 | --------------------------------------------------------------------------------