├── docs ├── console.md ├── index.md ├── routing.md └── quickstart.md ├── .gitignore ├── .travis.yml ├── src └── Jade │ ├── ServiceProviderInterface.php │ ├── Console │ ├── Command │ │ └── Command.php │ ├── CommandDiscovererProvider.php │ └── Application.php │ ├── Exception │ ├── ContainerException.php │ ├── ContainerValueNotFoundException.php │ ├── HttpException.php │ ├── NotFoundHttpException.php │ └── MethodNotAllowedHttpException.php │ ├── ContainerAwareInterface.php │ ├── ContainerAwareTrait.php │ ├── CommandProviderInterface.php │ ├── EventProviderInterface.php │ ├── HttpKernel │ ├── ArgumentResolverInterface.php │ ├── ControllerResolverInterface.php │ ├── KernelEvents.php │ ├── CallableRequestHandler.php │ ├── Event │ │ ├── PostResponseEvent.php │ │ ├── FilterControllerEvent.php │ │ ├── KernelEvent.php │ │ ├── FilterResponseEvent.php │ │ └── GetResponseEvent.php │ ├── EmitterDecorator.php │ ├── ControllerResolver.php │ ├── HttpKernelProvider.php │ ├── ArgumentResolver.php │ ├── RouterListener.php │ └── HttpKernel.php │ ├── ContainerInterface.php │ ├── Middleware │ ├── ErrorHandlerMiddleware.php │ ├── MiddlewareFactory.php │ └── RouteMiddleware.php │ ├── Twig │ ├── TwigServiceProvider.php │ ├── TwigExtension.php │ └── Twig.php │ ├── CoreServiceProvider.php │ ├── Provider │ ├── DoctrineExtensionsServiceProvider.php │ ├── AnnotationsServiceProvider.php │ ├── DoctrineServiceProvider.php │ ├── DoctrineCacheServiceProvider.php │ └── DoctrineOrmServiceProvider.php │ ├── Container.php │ ├── Routing │ ├── RouteCollection.php │ ├── RouteCollector.php │ ├── Route.php │ └── Router.php │ ├── Controller │ └── Controller.php │ └── App.php ├── phpunit.xml.dist ├── tests ├── ContainerTest.php ├── Middleware │ ├── MiddlewareFactoryTest.php │ └── RouteMiddlewareTest.php ├── Routing │ ├── RouteTest.php │ ├── RouteCollectionTest.php │ └── RouterTest.php └── AppTest.php ├── composer.json ├── .scrutinizer.yml └── README.md /docs/console.md: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | vendor/ 2 | .settings/ 3 | .project 4 | .buildpath 5 | composer.lock 6 | .idea 7 | tmp/ -------------------------------------------------------------------------------- /docs/index.md: -------------------------------------------------------------------------------- 1 | 2 | ## 章节 3 | 4 | - [QUICK START](./quickstart.md) 5 | - [路由](./routing.md) 6 | - [中间件](./) 7 | - [依赖注入](./) 8 | - [Console](./console.md) -------------------------------------------------------------------------------- /docs/routing.md: -------------------------------------------------------------------------------- 1 | 2 | ## 关于 Routing 3 | 4 | SF routing 功能基于[nikic/fastroute](https://github.com/nikic/FastRoute), 这是被lumen, slim 5 | 认可为效率最快的路由组件。 6 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: php 2 | 3 | php: 4 | - 7.1 5 | - 7.2 6 | - 7.3 7 | 8 | before_script: 9 | - composer install 10 | 11 | script: ./vendor/bin/phpunit --coverage-clover=coverage.xml 12 | 13 | after_success: 14 | - bash <(curl -s https://codecov.io/bash) 15 | 16 | notifications: 17 | email: false -------------------------------------------------------------------------------- /src/Jade/ServiceProviderInterface.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | namespace Jade; 13 | 14 | interface ServiceProviderInterface 15 | { 16 | public function register(ContainerInterface $container); 17 | } -------------------------------------------------------------------------------- /src/Jade/Console/Command/Command.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | namespace Jade\Console\Command; 13 | 14 | use Symfony\Component\Console\Command\Command as SymfonyCommand; 15 | 16 | class Command extends SymfonyCommand 17 | { 18 | } -------------------------------------------------------------------------------- /src/Jade/Exception/ContainerException.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | namespace Jade\Exception; 13 | 14 | use Psr\Container\ContainerExceptionInterface; 15 | 16 | class ContainerException extends \InvalidArgumentException implements ContainerExceptionInterface 17 | { 18 | } -------------------------------------------------------------------------------- /src/Jade/Exception/ContainerValueNotFoundException.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | namespace Jade\Exception; 13 | 14 | use Psr\Container\NotFoundExceptionInterface; 15 | 16 | class ContainerValueNotFoundException extends \RuntimeException implements NotFoundExceptionInterface 17 | { 18 | } -------------------------------------------------------------------------------- /src/Jade/ContainerAwareInterface.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | namespace Jade; 13 | 14 | interface ContainerAwareInterface 15 | { 16 | /** 17 | * Sets the container. 18 | * 19 | * @param ContainerInterface $container 20 | */ 21 | public function setContainer(ContainerInterface $container); 22 | } -------------------------------------------------------------------------------- /docs/quickstart.md: -------------------------------------------------------------------------------- 1 | 2 | # 快速开始 3 | 4 | SF 是个微内核框架,并不限制你的应用结构,你甚至可以在一个单文件里完成 `$app` 对象的构建; 5 | 6 | ```php 7 | use Psr\Http\Message\ServerRequestInterface; 8 | use Jade\App; 9 | use Zend\Diactoros\Response; 10 | 11 | $app = new App(); 12 | 13 | // 路由 14 | $app->get('/ping', function(ServerRequestInterface $request){ 15 | return new Response\TextResponse('pong'); 16 | }); 17 | $app->get('/greet/{name}', function(ServerRequestInterface $request, $name){ 18 | return new Response\TextResponse(sprintf('Hi %s!', $name)); 19 | }); 20 | 21 | $app->serve(); 22 | ``` 23 | -------------------------------------------------------------------------------- /src/Jade/ContainerAwareTrait.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | namespace Jade; 13 | 14 | trait ContainerAwareTrait 15 | { 16 | /** 17 | * @var ContainerInterface 18 | */ 19 | protected $container; 20 | 21 | public function setContainer(ContainerInterface $container) 22 | { 23 | $this->container = $container; 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /src/Jade/Exception/HttpException.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | namespace Jade\Exception; 13 | 14 | use Throwable; 15 | 16 | class HttpException extends \Exception 17 | { 18 | public function __construct(string $message = "", int $code = 0, Throwable $previous = null) 19 | { 20 | parent::__construct($message, $code, $previous); 21 | } 22 | } -------------------------------------------------------------------------------- /src/Jade/CommandProviderInterface.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | namespace Jade; 13 | 14 | use Jade\Console\Application; 15 | 16 | interface CommandProviderInterface 17 | { 18 | /** 19 | * 注册命令 20 | * 21 | * @param Application $app 22 | * @param ContainerInterface $container 23 | */ 24 | public function provide(Application $app, ContainerInterface $container); 25 | } -------------------------------------------------------------------------------- /src/Jade/EventProviderInterface.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | namespace Jade; 13 | 14 | use Symfony\Component\EventDispatcher\EventDispatcherInterface; 15 | 16 | interface EventProviderInterface 17 | { 18 | /** 19 | * 注册一组事件 20 | * 21 | * @param EventDispatcherInterface $eventDispatcher 22 | * @param ContainerInterface $container 23 | */ 24 | public function subscribe(EventDispatcherInterface $eventDispatcher, ContainerInterface $container); 25 | } -------------------------------------------------------------------------------- /src/Jade/HttpKernel/ArgumentResolverInterface.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | namespace Jade\HttpKernel; 13 | 14 | use Psr\Http\Message\ServerRequestInterface; 15 | 16 | interface ArgumentResolverInterface 17 | { 18 | /** 19 | * 获取 controller 的参数 20 | * @param ServerRequestInterface $request 21 | * @param callable $controller 22 | * @return array 23 | */ 24 | public function getArguments(ServerRequestInterface $request, callable $controller); 25 | } -------------------------------------------------------------------------------- /src/Jade/Exception/NotFoundHttpException.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | namespace Jade\Exception; 13 | 14 | class NotFoundHttpException extends HttpException 15 | { 16 | /** 17 | * @param string $message The internal exception message 18 | * @param \Exception $previous The previous exception 19 | */ 20 | public function __construct($message = null, \Exception $previous = null) 21 | { 22 | parent::__construct($message, 404, $previous); 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /src/Jade/ContainerInterface.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | namespace Jade; 13 | 14 | use Psr\Container\ContainerInterface as PsrContainerInterface; 15 | 16 | interface ContainerInterface extends PsrContainerInterface 17 | { 18 | /** 19 | * 批量添加服务/参数 20 | * 21 | * @param array $values 22 | */ 23 | public function add(array $values); 24 | 25 | /** 26 | * 批量合并参数 27 | * 28 | * @param array $values 29 | */ 30 | public function merge(array $values); 31 | } -------------------------------------------------------------------------------- /src/Jade/Exception/MethodNotAllowedHttpException.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | namespace Jade\Exception; 13 | 14 | class MethodNotAllowedHttpException extends HttpException 15 | { 16 | /** 17 | * @param string $message The internal exception message 18 | * @param \Exception $previous The previous exception 19 | */ 20 | public function __construct($message = null, \Exception $previous = null) 21 | { 22 | parent::__construct($message, 405, $previous); 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /phpunit.xml.dist: -------------------------------------------------------------------------------- 1 | 2 | 7 | 8 | 9 | 10 | ./tests/ 11 | 12 | 13 | 14 | 15 | 16 | ./ 17 | 18 | ./tests 19 | ./vendor 20 | 21 | 22 | 23 | -------------------------------------------------------------------------------- /src/Jade/HttpKernel/ControllerResolverInterface.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | namespace Jade\HttpKernel; 13 | 14 | use Psr\Http\Message\ServerRequestInterface; 15 | 16 | interface ControllerResolverInterface 17 | { 18 | /** 19 | * 控制器资源标识结构 20 | * 21 | * 例如: 22 | * 23 | * ``` 24 | * PostController::indexAction 25 | * ``` 26 | * 27 | * @param ServerRequestInterface $request 28 | * @return callable 返回一个合理有效的php可执行结构 29 | * @throws \RuntimeException 30 | */ 31 | public function getController(ServerRequestInterface $request); 32 | } -------------------------------------------------------------------------------- /src/Jade/Middleware/ErrorHandlerMiddleware.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | namespace Jade\Middleware; 13 | 14 | use Psr\Http\Message\ResponseInterface; 15 | use Psr\Http\Message\ServerRequestInterface; 16 | use Psr\Http\Server\MiddlewareInterface; 17 | use Psr\Http\Server\RequestHandlerInterface; 18 | use Symfony\Component\Debug\Debug; 19 | 20 | class ErrorHandlerMiddleware implements MiddlewareInterface 21 | { 22 | public function process(ServerRequestInterface $request, RequestHandlerInterface $handler): ResponseInterface 23 | { 24 | Debug::enable(); 25 | return $handler->handle($request); 26 | } 27 | } -------------------------------------------------------------------------------- /src/Jade/HttpKernel/KernelEvents.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | namespace Jade\HttpKernel; 13 | 14 | final class KernelEvents 15 | { 16 | /** 17 | * 请求达到时触发 18 | */ 19 | const REQUEST = 'kernel.request'; 20 | 21 | /** 22 | * 请求经 middleware 调度结束时触发 23 | */ 24 | const MIDDLEWARE = 'kernel.middle'; 25 | 26 | /** 27 | * 异常时触发 28 | */ 29 | const EXCEPTION = 'kernel.exception'; 30 | 31 | /** 32 | * 请求达到控制器时触发 33 | */ 34 | const CONTROLLER = 'kernel.controller'; 35 | 36 | /** 37 | * 请求结束,响应生成时触发 38 | */ 39 | const RESPONSE = 'kernel.response'; 40 | 41 | /** 42 | * 响应输出时触发 43 | */ 44 | const TERMINATE = 'kernel.terminate'; 45 | } -------------------------------------------------------------------------------- /src/Jade/Twig/TwigServiceProvider.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | namespace Jade\Twig; 13 | 14 | use Jade\ContainerInterface; 15 | use Jade\ServiceProviderInterface; 16 | 17 | class TwigServiceProvider implements ServiceProviderInterface 18 | { 19 | public function register(ContainerInterface $container) 20 | { 21 | $container->add([ 22 | 'twig.class' => Twig::class, 23 | 'twig.paths' => [], 24 | 'twig.environment' => [], 25 | ]); 26 | $container['twig_class'] = Twig::class; 27 | 28 | $container['twig'] = function($c){ 29 | $twig = new $c['twig_class']($c['twig.paths'], $c['twig.environment']); 30 | return $twig; 31 | }; 32 | } 33 | } -------------------------------------------------------------------------------- /src/Jade/HttpKernel/CallableRequestHandler.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | namespace Jade\HttpKernel; 13 | 14 | use Psr\Http\Message\ResponseInterface; 15 | use Psr\Http\Message\ServerRequestInterface; 16 | use Psr\Http\Server\RequestHandlerInterface; 17 | 18 | class CallableRequestHandler implements RequestHandlerInterface 19 | { 20 | /** 21 | * @var callable 22 | */ 23 | protected $callback; 24 | 25 | public function __construct(callable $callback) 26 | { 27 | $this->callback = $callback; 28 | } 29 | 30 | /** 31 | * {@inheritdoc} 32 | */ 33 | public function handle(ServerRequestInterface $request): ResponseInterface 34 | { 35 | return call_user_func($this->callback, $request); 36 | } 37 | } -------------------------------------------------------------------------------- /src/Jade/HttpKernel/Event/PostResponseEvent.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | namespace Jade\HttpKernel\Event; 13 | 14 | use Psr\Http\Message\ResponseInterface; 15 | use Psr\Http\Message\ServerRequestInterface; 16 | use Jade\HttpKernel\HttpKernel; 17 | 18 | class PostResponseEvent extends KernelEvent 19 | { 20 | protected $response; 21 | 22 | public function __construct(HttpKernel $kernel, ServerRequestInterface $request, ResponseInterface $response) 23 | { 24 | parent::__construct($kernel, $request); 25 | $this->response = $response; 26 | } 27 | 28 | /** 29 | * 返回当前处理得到的response 30 | * 31 | * @return ResponseInterface 32 | */ 33 | public function getResponse() 34 | { 35 | return $this->response; 36 | } 37 | } -------------------------------------------------------------------------------- /tests/ContainerTest.php: -------------------------------------------------------------------------------- 1 | container = new Container(); 19 | } 20 | 21 | public function testGet() 22 | { 23 | $this->container['foo'] = function(){ 24 | return 'bar'; 25 | }; 26 | $this->assertSame('bar', $this->container->get('foo')); 27 | } 28 | 29 | public function testGetWithValueNotFoundError() 30 | { 31 | $this->expectException(ContainerValueNotFoundException::class); 32 | $this->container->get('foo'); 33 | } 34 | 35 | public function testHas() 36 | { 37 | $this->assertFalse($this->container->has('foo')); 38 | $this->container['foo'] = function(){ 39 | return 'bar'; 40 | }; 41 | $this->assertTrue($this->container->has('foo')); 42 | } 43 | } -------------------------------------------------------------------------------- /src/Jade/HttpKernel/Event/FilterControllerEvent.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | namespace Jade\HttpKernel\Event; 13 | 14 | use Psr\Http\Message\ServerRequestInterface; 15 | use Jade\HttpKernel\HttpKernel; 16 | 17 | /** 18 | * 允许重新修改 19 | */ 20 | class FilterControllerEvent extends KernelEvent 21 | { 22 | protected $controller; 23 | 24 | public function __construct(HttpKernel $kernel, ServerRequestInterface $request, callable $controller) 25 | { 26 | parent::__construct($kernel, $request); 27 | $this->setController($controller); 28 | } 29 | 30 | /** 31 | * Returns the current controller. 32 | * 33 | * @return callable 34 | */ 35 | public function getController() 36 | { 37 | return $this->controller; 38 | } 39 | 40 | public function setController(callable $controller) 41 | { 42 | $this->controller = $controller; 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "jadephp/jade", 3 | "description": "Jade is a flexible PHP micro framework", 4 | "keywords": ["php-framework", "micro-framework", "jade", "psr-7", "psr-15"], 5 | "type": "library", 6 | "license": "MIT", 7 | "authors": [ 8 | { 9 | "name": "Slince", 10 | "email": "taosikai@yeah.net" 11 | } 12 | ], 13 | "require": { 14 | "php": "^7.1.3", 15 | "pimple/pimple": "^3.2", 16 | "nikic/fast-route": "^1.3", 17 | "symfony/event-dispatcher": "^4.0", 18 | "symfony/console": "^4.0", 19 | "zendframework/zend-stratigility": "^3.2", 20 | "zendframework/zend-diactoros": "^2.1", 21 | "zendframework/zend-httphandlerrunner": "^1.1" 22 | }, 23 | "require-dev": { 24 | "phpunit/phpunit": "^6.0", 25 | "symfony/var-dumper": "^4.3", 26 | "symfony/debug": "^4.3" 27 | }, 28 | "autoload": { 29 | "psr-4": { 30 | "Jade\\": "./src/Jade" 31 | } 32 | }, 33 | "autoload-dev": { 34 | "psr-4": { 35 | "Jade\\Tests\\": "./tests" 36 | } 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /src/Jade/CoreServiceProvider.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | namespace Jade; 13 | 14 | use Jade\Middleware\MiddlewareFactory; 15 | use Symfony\Component\EventDispatcher\EventDispatcher; 16 | use Zend\Stratigility\MiddlewarePipe; 17 | 18 | class CoreServiceProvider implements ServiceProviderInterface 19 | { 20 | public function register(ContainerInterface $container) 21 | { 22 | // 事件管理 23 | $container['event_dispatcher'] = function(){ 24 | return new EventDispatcher(); 25 | }; 26 | // middleware 控制 27 | $container['middleware_pipeline'] = function(){ 28 | return new MiddlewarePipe(); 29 | }; 30 | // middleware 控制 31 | $container['middleware_factory'] = function(){ 32 | return new MiddlewareFactory(); 33 | }; 34 | // route collector 35 | $container['route_collector'] = $container->get('app'); 36 | } 37 | } -------------------------------------------------------------------------------- /src/Jade/HttpKernel/Event/KernelEvent.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | namespace Jade\HttpKernel\Event; 13 | 14 | use Psr\Http\Message\ServerRequestInterface; 15 | use Jade\HttpKernel\HttpKernel; 16 | use Symfony\Contracts\EventDispatcher\Event; 17 | 18 | class KernelEvent extends Event 19 | { 20 | protected $kernel; 21 | protected $request; 22 | 23 | public function __construct(HttpKernel $kernel, ServerRequestInterface $request) 24 | { 25 | $this->kernel = $kernel; 26 | $this->request = $request; 27 | } 28 | 29 | /** 30 | * 返回 http kernel 31 | * 32 | * @return HttpKernel 33 | */ 34 | public function getKernel() 35 | { 36 | return $this->kernel; 37 | } 38 | 39 | /** 40 | * 返回当前正在处理中的请求 41 | * 42 | * @return ServerRequestInterface 43 | */ 44 | public function getRequest() 45 | { 46 | return $this->request; 47 | } 48 | } -------------------------------------------------------------------------------- /src/Jade/Middleware/MiddlewareFactory.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | namespace Jade\Middleware; 13 | 14 | use Psr\Http\Server\MiddlewareInterface; 15 | use Zend\Stratigility\Middleware\CallableMiddlewareDecorator; 16 | 17 | final class MiddlewareFactory 18 | { 19 | /** 20 | * 创建middleware 21 | * 22 | * @param MiddlewareInterface|string|callable $middleware 23 | * @return MiddlewareInterface 24 | */ 25 | public function create($middleware) 26 | { 27 | // middleware 实例直接返回 28 | if ($middleware instanceof MiddlewareInterface) { 29 | return $middleware; 30 | } 31 | // middleware 类 32 | if (is_subclass_of($middleware, MiddlewareInterface::class)) { 33 | return new $middleware; 34 | } 35 | if (is_callable($middleware)) { 36 | return new CallableMiddlewareDecorator($middleware); 37 | } 38 | throw new \InvalidArgumentException('Invalid middleware format'); 39 | } 40 | } -------------------------------------------------------------------------------- /.scrutinizer.yml: -------------------------------------------------------------------------------- 1 | filter: 2 | excluded_paths: 3 | - "tests/*" 4 | - "vendor/*" 5 | 6 | tools: 7 | external_code_coverage: 8 | timeout: 600 9 | 10 | php_sim: true 11 | 12 | php_changetracking: true 13 | 14 | php_cs_fixer: 15 | enabled: true 16 | config: 17 | level: all 18 | filter: 19 | excluded_paths: 20 | - "tests/*" 21 | - "vendor/*" 22 | 23 | php_mess_detector: 24 | enabled: true 25 | filter: 26 | excluded_paths: 27 | - "tests/*" 28 | - "vendor/*" 29 | 30 | php_pdepend: 31 | enabled: true 32 | filter: 33 | excluded_paths: 34 | - "tests/*" 35 | - "vendor/*" 36 | 37 | php_analyzer: 38 | enabled: true 39 | filter: 40 | excluded_paths: 41 | - "tests/*" 42 | - "vendor/*" 43 | 44 | 45 | php_cpd: 46 | enabled: true 47 | excluded_dirs: 48 | - "tests/*" 49 | - "vendor/*" 50 | 51 | php_loc: 52 | enabled: true 53 | excluded_dirs: 54 | - "tests/*" 55 | - "vendor/*" -------------------------------------------------------------------------------- /tests/Middleware/MiddlewareFactoryTest.php: -------------------------------------------------------------------------------- 1 | assertInstanceOf(MiddlewareInterface::class, $factory->create(function(){ 26 | // ignore 27 | })); 28 | 29 | $middleware= new TestMiddleware(); 30 | $this->assertInstanceOf(MiddlewareInterface::class, $factory->create($middleware)); 31 | 32 | $this->assertInstanceOf(MiddlewareInterface::class, $factory->create(TestMiddleware::class)); 33 | 34 | $this->expectException(\InvalidArgumentException::class); 35 | $factory->create('foo'); 36 | } 37 | } -------------------------------------------------------------------------------- /src/Jade/HttpKernel/Event/FilterResponseEvent.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | namespace Jade\HttpKernel\Event; 13 | 14 | use Psr\Http\Message\ResponseInterface; 15 | use Psr\Http\Message\ServerRequestInterface; 16 | use Jade\HttpKernel\HttpKernel; 17 | 18 | /** 19 | * 允许动态修改内核周期结束之后产生的response 20 | */ 21 | class FilterResponseEvent extends KernelEvent 22 | { 23 | protected $response; 24 | 25 | public function __construct(HttpKernel $kernel, ServerRequestInterface $request, ResponseInterface $response) 26 | { 27 | parent::__construct($kernel, $request); 28 | $this->response = $response; 29 | } 30 | 31 | /** 32 | * 返回当前处理得到的response 33 | * 34 | * @return ResponseInterface 35 | */ 36 | public function getResponse() 37 | { 38 | return $this->response; 39 | } 40 | 41 | /** 42 | * 设置一个新的response对象来替换 43 | * 44 | * @param ResponseInterface $response 45 | */ 46 | public function setResponse(ResponseInterface $response) 47 | { 48 | $this->response = $response; 49 | } 50 | } -------------------------------------------------------------------------------- /src/Jade/Middleware/RouteMiddleware.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | namespace Jade\Middleware; 13 | 14 | use Psr\Http\Message\ResponseInterface; 15 | use Psr\Http\Message\ServerRequestInterface; 16 | use Psr\Http\Server\MiddlewareInterface; 17 | use Psr\Http\Server\RequestHandlerInterface; 18 | use Jade\Routing\Route; 19 | 20 | class RouteMiddleware implements MiddlewareInterface 21 | { 22 | /** 23 | * @var Route 24 | */ 25 | protected $route; 26 | 27 | /** 28 | * @var MiddlewareInterface 29 | */ 30 | protected $decorated; 31 | 32 | public function __construct(Route $route, $decoratedMiddleware) 33 | { 34 | $this->route = $route; 35 | $this->decorated = $decoratedMiddleware; 36 | } 37 | 38 | public function process(ServerRequestInterface $request, RequestHandlerInterface $handler): ResponseInterface 39 | { 40 | if ($this->route === $request->getAttribute('_route')) { 41 | return $this->decorated->process($request, $handler); 42 | } 43 | return $handler->handle($request); 44 | } 45 | } -------------------------------------------------------------------------------- /src/Jade/HttpKernel/EmitterDecorator.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | namespace Jade\HttpKernel; 13 | 14 | use Psr\Http\Message\ResponseInterface; 15 | use Zend\HttpHandlerRunner\Emitter\EmitterInterface; 16 | use Zend\HttpHandlerRunner\Emitter\SapiEmitter; 17 | use Zend\HttpHandlerRunner\Emitter\SapiStreamEmitter; 18 | 19 | class EmitterDecorator implements EmitterInterface 20 | { 21 | protected $emitter; 22 | protected $streamEmitter; 23 | 24 | public function __construct(SapiEmitter $emitter, SapiStreamEmitter $streamEmitter) 25 | { 26 | $this->emitter = $emitter; 27 | $this->streamEmitter = $streamEmitter; 28 | } 29 | 30 | /** 31 | * {@inheritdoc} 32 | */ 33 | public function emit(ResponseInterface $response) : bool 34 | { 35 | if (! $response->hasHeader('Content-Disposition') 36 | && ! $response->hasHeader('Content-Range') 37 | ) { 38 | return $this->emitter->emit($response); 39 | } 40 | // 内存优化型 response 输出 41 | return $this->streamEmitter->emit($response); 42 | } 43 | } -------------------------------------------------------------------------------- /src/Jade/HttpKernel/Event/GetResponseEvent.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | namespace Jade\HttpKernel\Event; 13 | 14 | use Psr\Http\Message\ResponseInterface; 15 | use Psr\Http\Message\ServerRequestInterface; 16 | 17 | final class GetResponseEvent extends KernelEvent 18 | { 19 | /** 20 | * @var ResponseInterface 21 | */ 22 | protected $response; 23 | 24 | /** 25 | * 设置正在处理的请求,由于psr7 request 不可修改特性 26 | * 但凡对原request修改都要重新写回新的对象 27 | * 28 | * @param ServerRequestInterface $request 29 | */ 30 | public function setRequest(ServerRequestInterface $request): void 31 | { 32 | $this->request = $request; 33 | } 34 | 35 | /** 36 | * 设置响应体 37 | * 38 | * @param ResponseInterface $response 39 | */ 40 | public function setResponse(ResponseInterface $response) 41 | { 42 | $this->response = $response; 43 | } 44 | 45 | /** 46 | * 获取请求 47 | * 48 | * @return ResponseInterface 49 | */ 50 | public function getResponse(): ?ResponseInterface 51 | { 52 | return $this->response; 53 | } 54 | 55 | 56 | /** 57 | * 检查 response 是否存在 58 | * 59 | * @return bool 60 | */ 61 | public function hasResponse() 62 | { 63 | return null !== $this->response; 64 | } 65 | } -------------------------------------------------------------------------------- /src/Jade/Console/CommandDiscovererProvider.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | namespace Jade\Console; 13 | 14 | use Jade\CommandProviderInterface; 15 | use Psr\Container\ContainerInterface; 16 | use Zend\Stdlib\Glob; 17 | 18 | class CommandDiscovererProvider implements CommandProviderInterface 19 | { 20 | /** 21 | * @var string 22 | */ 23 | protected $dstDir; 24 | 25 | /** 26 | * @var string 27 | */ 28 | protected $namespace; 29 | 30 | public function __construct($namespace, $dstDir) 31 | { 32 | $this->namespace = rtrim($namespace, '\\'); 33 | $this->dstDir = rtrim($dstDir, '\\/'); 34 | } 35 | 36 | /** 37 | * {@inheritdoc} 38 | */ 39 | public function provide(Application $app, ContainerInterface $container) 40 | { 41 | foreach (Glob::glob("{$this->dstDir}/*Command.php") as $file) { 42 | $commands = []; 43 | try { 44 | $class = $this->namespace . pathinfo($file, PATHINFO_FILENAME); 45 | $r = new \ReflectionClass($class); 46 | if ($r->isSubclassOf('Symfony\\Component\\Console\\Command\\Command') && !$r->isAbstract() && !$r->getConstructor()->getNumberOfRequiredParameters()) { 47 | $command = $r->newInstance(); 48 | $commands[] = $command; 49 | } 50 | } catch (\ReflectionException $e) { 51 | // 忽略无法反射的命令 52 | } 53 | $app->addCommands($commands); 54 | } 55 | } 56 | } -------------------------------------------------------------------------------- /tests/Routing/RouteTest.php: -------------------------------------------------------------------------------- 1 | assertAttributeEquals($methods, 'methods', $route); 31 | $this->assertAttributeEquals($pattern, 'pattern', $route); 32 | $this->assertAttributeEquals($callable, 'action', $route); 33 | $this->assertAttributeEquals([], 'arguments', $route); 34 | } 35 | 36 | public function testGetMethods() 37 | { 38 | $this->assertEquals(['GET', 'POST'], $this->routeFactory()->getMethods()); 39 | } 40 | 41 | public function testGetPattern() 42 | { 43 | $this->assertEquals('/hello/{name}', $this->routeFactory()->getPattern()); 44 | } 45 | 46 | public function testGetAction() 47 | { 48 | $callable = $this->routeFactory()->getAction(); 49 | 50 | $this->assertInternalType('callable', $callable); 51 | } 52 | 53 | public function testGetName() 54 | { 55 | $name = $this->routeFactory()->getName(); 56 | $this->assertEquals('route1', $name); 57 | } 58 | } -------------------------------------------------------------------------------- /src/Jade/Provider/DoctrineExtensionsServiceProvider.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | namespace Jade\Provider; 13 | 14 | use Doctrine\Common\EventManager; 15 | use Gedmo\Sortable\SortableListener; 16 | use Gedmo\Timestampable\TimestampableListener; 17 | use Gedmo\Sluggable\SluggableListener; 18 | use Jade\ContainerInterface; 19 | use Jade\ServiceProviderInterface; 20 | 21 | /** 22 | * @author Sérgio Rafael Siquira 23 | */ 24 | class DoctrineExtensionsServiceProvider implements ServiceProviderInterface 25 | { 26 | public function register(ContainerInterface $container) 27 | { 28 | if (!isset($container['annotations'])) { 29 | throw new \LogicException( 30 | 'You must register the AnnotationsServiceProvider to use the DoctrineExtensionsServiceProvider.' 31 | ); 32 | } 33 | 34 | $container['gedmo.listeners'] = function () { 35 | return [ 36 | new SortableListener(), 37 | new TimestampableListener(), 38 | new SluggableListener(), 39 | ]; 40 | }; 41 | 42 | $container['db.event_manager'] = $container->extend('db.event_manager', function (EventManager $event) use ($container) { 43 | $listeners = $container['gedmo.listeners']; 44 | 45 | foreach ($listeners as $listener) { 46 | $listener->setAnnotationReader($container['annotations']); 47 | $event->addEventSubscriber($listener); 48 | } 49 | 50 | return $event; 51 | }); 52 | } 53 | } -------------------------------------------------------------------------------- /tests/Middleware/RouteMiddlewareTest.php: -------------------------------------------------------------------------------- 1 | pipe(new RouteMiddleware($route, new FooMiddleware())); 33 | 34 | $request = new ServerRequest(); 35 | $response = $pipeline->handle($request->withAttribute('_route', $route)); 36 | $this->assertInstanceOf(ResponseInterface::class, $response); 37 | $this->assertEquals('foo_middleware', (string)$response->getBody()); 38 | } 39 | 40 | public function testExecuteNotMatched() 41 | { 42 | $pipeline = new MiddlewarePipe(); 43 | 44 | $route = new Route('route1', '/foo', function(){}, ['GET']); 45 | $pipeline->pipe(new RouteMiddleware($route, new FooMiddleware())); 46 | 47 | // 不匹配的情况 48 | $request = new ServerRequest(); 49 | $this->expectException(EmptyPipelineException::class); 50 | $pipeline->handle($request); 51 | } 52 | } -------------------------------------------------------------------------------- /src/Jade/Console/Application.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | namespace Jade\Console; 13 | 14 | use Jade\App; 15 | use Jade\CommandProviderInterface; 16 | use Symfony\Component\Console\Application as SymfonyApplication; 17 | use Symfony\Component\Console\Input\InputInterface; 18 | use Symfony\Component\Console\Output\OutputInterface; 19 | 20 | class Application extends SymfonyApplication 21 | { 22 | /** 23 | * Logo Text 24 | * 25 | * @var string 26 | */ 27 | const LOGO = <<decorated = $app; 44 | parent::__construct('Jade', '0.0.1'); 45 | } 46 | 47 | /** 48 | * {@inheritdoc} 49 | */ 50 | public function getHelp() 51 | { 52 | return parent::getHelp() . PHP_EOL . static::LOGO; 53 | } 54 | 55 | /** 56 | * {@inheritdoc} 57 | */ 58 | public function doRun(InputInterface $input, OutputInterface $output) 59 | { 60 | $this->decorated->boot(); 61 | 62 | $this->registerCommands(); 63 | 64 | return parent::doRun($input, $output); 65 | } 66 | 67 | protected function registerCommands() 68 | { 69 | foreach ($this->decorated->getProviders() as $provider) { 70 | if ($provider instanceof CommandProviderInterface) { 71 | $provider->provide($this, $this->decorated->getContainer()); 72 | } 73 | } 74 | } 75 | } -------------------------------------------------------------------------------- /src/Jade/Container.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | namespace Jade; 13 | 14 | use Pimple\Container as PimpleContainer; 15 | use Jade\Exception\ContainerException; 16 | use Jade\Exception\ContainerValueNotFoundException; 17 | 18 | class Container extends PimpleContainer implements ContainerInterface 19 | { 20 | /** 21 | * 从容器中获取实例对象或者其它资源 22 | * 23 | * @param string $id 24 | * @return mixed 25 | */ 26 | public function get($id) 27 | { 28 | if (!$this->offsetExists($id)) { 29 | throw new ContainerValueNotFoundException(sprintf('Identifier "%s" is not defined.', $id)); 30 | } 31 | try { 32 | return $this->offsetGet($id); 33 | } catch (\InvalidArgumentException $exception) { 34 | throw new ContainerException(sprintf('Container error while retrieving "%s"', $id), 35 | null, 36 | $exception 37 | ); 38 | } 39 | } 40 | 41 | /** 42 | * 检查容器是否存储该资源,如果存在返回true,否则返回false 43 | * 44 | * @param string $id Identifier of the entry to look for. 45 | * 46 | * @return boolean 47 | */ 48 | public function has($id) 49 | { 50 | return $this->offsetExists($id); 51 | } 52 | 53 | /** 54 | * {@inheritdoc} 55 | */ 56 | public function merge(array $values) 57 | { 58 | foreach ($values as $key => $value) { 59 | $this[$key] = $value; 60 | } 61 | } 62 | 63 | /** 64 | * {@inheritdoc} 65 | */ 66 | public function add(array $values) 67 | { 68 | foreach ($values as $key => $value) { 69 | $this->offsetExists($key) || $this[$key] = $value; 70 | } 71 | } 72 | } -------------------------------------------------------------------------------- /src/Jade/Routing/RouteCollection.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | namespace Jade\Routing; 13 | 14 | final class RouteCollection implements \Countable, \IteratorAggregate 15 | { 16 | /** 17 | * 路由集合 18 | * @var Route[] 19 | */ 20 | protected $routes = []; 21 | 22 | /** 23 | * 路由计数 24 | * 25 | * @var int 26 | */ 27 | protected $counter = 0; 28 | 29 | public function __construct($routes = []) 30 | { 31 | foreach ($routes as $route) { 32 | $this->add($route); 33 | } 34 | } 35 | 36 | /** 37 | * 添加一条 route 38 | * 39 | * @param Route $route 40 | */ 41 | public function add(Route $route) 42 | { 43 | if (null === $route->getName()) { //路由名称为空则给予默认名称 44 | $route->setName($this->counter ++); 45 | } 46 | $this->routes[$route->getName()] = $route; 47 | } 48 | 49 | /** 50 | * 查找 route 51 | * 52 | * @param string $name 53 | * @return Route|null 54 | */ 55 | public function search($name) 56 | { 57 | return $this->routes[$name] ?? null; 58 | } 59 | 60 | /** 61 | * 合并 route 集合 62 | * 63 | * @param RouteCollection $collection 64 | */ 65 | public function merge(RouteCollection $collection) 66 | { 67 | foreach ($collection as $route) { 68 | $this->add($route); 69 | } 70 | } 71 | 72 | /** 73 | * 获取集合内路由总数 74 | * 75 | * @return int 76 | */ 77 | public function count() 78 | { 79 | return count($this->routes); 80 | } 81 | 82 | /** 83 | * {@inheritdoc} 84 | */ 85 | public function getIterator() 86 | { 87 | return new \ArrayIterator($this->routes); 88 | } 89 | } -------------------------------------------------------------------------------- /tests/Routing/RouteCollectionTest.php: -------------------------------------------------------------------------------- 1 | assertCount(0, $routes); 15 | $this->assertInstanceOf(\Traversable::class, $routes); 16 | } 17 | public function testAdd() 18 | { 19 | $routes = new RouteCollection(); 20 | $routes->add(new Route('route1', '/foo', function(){ 21 | }, ['GET'])); 22 | $this->assertCount(1, $routes); 23 | 24 | $routes->add(new Route('route2', '/foo', function(){ 25 | }, ['GET'])); 26 | 27 | $this->assertCount(2, $routes); 28 | } 29 | 30 | public function testAddSameNameRoute() 31 | { 32 | $routes = new RouteCollection(); 33 | $routes->add(new Route('route1', '/foo', function(){ 34 | }, ['GET'])); 35 | $this->assertCount(1, $routes); 36 | // 加同名路由会覆盖 37 | $routes->add(new Route('route1', '/foo', function(){ 38 | }, ['GET'])); 39 | $this->assertCount(1, $routes); 40 | } 41 | 42 | public function testSearch() 43 | { 44 | $routes = new RouteCollection(); 45 | $route = new Route('route1', '/foo', function(){ 46 | }, ['GET']); 47 | $routes->add($route); 48 | $this->assertSame($route, $routes->search('route1')); 49 | } 50 | 51 | public function testMerge() 52 | { 53 | $routes = new RouteCollection(); 54 | $route = new Route('route1', '/foo', function(){ 55 | }, ['GET']); 56 | $routes->add($route); 57 | 58 | $routes2 = new RouteCollection(); 59 | $route2 = new Route('route2', '/foo', function(){ 60 | }, ['GET']); 61 | $routes2->add($route2); 62 | 63 | $routes->merge($routes2); 64 | $this->assertCount(2, $routes); 65 | } 66 | } -------------------------------------------------------------------------------- /src/Jade/Controller/Controller.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | namespace Jade\Controller; 13 | 14 | use Jade\ContainerAwareInterface; 15 | use Jade\ContainerAwareTrait; 16 | use Zend\Diactoros\Response; 17 | 18 | class Controller implements ContainerAwareInterface 19 | { 20 | use ContainerAwareTrait; 21 | 22 | /** 23 | * 渲染模板 24 | * @param string $view 25 | * @param array $parameters 26 | * @return string 27 | */ 28 | protected function renderView($view, array $parameters = []) 29 | { 30 | return $this->container->get('twig')->render($view, $parameters); 31 | } 32 | 33 | /** 34 | * 渲染模板 35 | * 36 | * @param string $view 37 | * @param array $parameters 38 | * @return Response\HtmlResponse 39 | */ 40 | protected function render($view, array $parameters = []) 41 | { 42 | $content = $this->renderView($view, $parameters); 43 | return new Response\HtmlResponse($content); 44 | } 45 | 46 | /** 47 | * @param string $id 48 | * @return mixed 49 | */ 50 | protected function get($id) 51 | { 52 | return $this->container->get($id); 53 | } 54 | 55 | /** 56 | * Returns a JsonResponse that uses the serializer component if enabled, or json_encode. 57 | * 58 | * @final 59 | */ 60 | protected function json($data, int $status = 200, array $headers = [], array $context = []) 61 | { 62 | if ($this->container->has('serializer')) { 63 | $json = $this->container->get('serializer')->serialize($data, 'json', array_merge([ 64 | 'json_encode_options' => Response\JsonResponse::DEFAULT_JSON_FLAGS, 65 | ], $context)); 66 | } 67 | return new Response\JsonResponse($data, $status, $headers); 68 | } 69 | } -------------------------------------------------------------------------------- /src/Jade/Provider/AnnotationsServiceProvider.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | namespace Jade\Provider; 13 | 14 | use Doctrine\Common\Annotations\CachedReader; 15 | use Doctrine\Common\Annotations\AnnotationReader; 16 | use Jade\ContainerInterface; 17 | use Jade\ServiceProviderInterface; 18 | 19 | /** 20 | * @author Sérgio Rafael Siqueira 21 | */ 22 | class AnnotationsServiceProvider implements ServiceProviderInterface 23 | { 24 | public function register(ContainerInterface $container) 25 | { 26 | $container->add([ 27 | 'annotations.debug' => false, 28 | 'annotations.options' => [ 29 | 'cache_driver' => 'array', 30 | 'cache_dir' => null, 31 | ] 32 | ]); 33 | $container['annotation_reader'] = function () use ($container) { 34 | return new AnnotationReader(); 35 | }; 36 | 37 | $container['annotations.cached_reader.factory'] = $container->protect(function ($options) use ($container) { 38 | if (!isset($container['cache'])) { 39 | throw new \LogicException( 40 | 'You must register the DoctrineCacheServiceProvider to use the AnnotationServiceProvider.' 41 | ); 42 | } 43 | 44 | return $container['cache_factory']($options['cache_driver'], $options); 45 | }); 46 | 47 | $container['annotations.cached_reader'] = function () use ($container) { 48 | return new CachedReader( 49 | $container['annotation_reader'], 50 | $container['annotations.cached_reader.factory']($container['annotations.options']), 51 | $container['annotations.debug'] 52 | ); 53 | }; 54 | 55 | $container['annotations'] = function () use ($container) { 56 | return $container['annotations.cached_reader']; 57 | }; 58 | } 59 | } -------------------------------------------------------------------------------- /src/Jade/HttpKernel/ControllerResolver.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | namespace Jade\HttpKernel; 13 | 14 | use Jade\ContainerAwareInterface; 15 | use Jade\ContainerInterface; 16 | use Psr\Http\Message\ServerRequestInterface; 17 | 18 | class ControllerResolver implements ControllerResolverInterface 19 | { 20 | /** 21 | * @var ContainerInterface 22 | */ 23 | protected $container; 24 | 25 | public function __construct(ContainerInterface $container) 26 | { 27 | $this->container = $container; 28 | } 29 | 30 | /** 31 | * {@inheritdoc} 32 | */ 33 | public function getController(ServerRequestInterface $request) 34 | { 35 | $route = $request->getAttribute('_route'); 36 | if (null === $route) { //如果没有route则直接中断 37 | throw new \RuntimeException(sprintf('Cannot find route')); 38 | } 39 | $action = $route->getAction(); 40 | if ($action instanceof \Closure) { // 如果是可调用的结构直接返回 41 | return $action; 42 | } 43 | return $this->createController($action); 44 | } 45 | 46 | /** 47 | * 创建控制器 48 | * 49 | * @param string|array $controller 50 | * @return array 51 | */ 52 | protected function createController($controller) 53 | { 54 | list($class, $method) = is_string($controller) ? explode('::', $controller) : $controller; 55 | 56 | if (!class_exists($class)) { 57 | throw new \InvalidArgumentException(sprintf('Class "%s" does not exist.', $class)); 58 | } 59 | return [$this->configureController($this->instantiateController($class)), $method]; 60 | } 61 | 62 | /** 63 | * 创建控制器实例 64 | * 65 | * @param string $class A class name 66 | * 67 | * @return object 68 | */ 69 | protected function instantiateController($class) 70 | { 71 | return new $class(); 72 | } 73 | 74 | protected function configureController($controller) 75 | { 76 | if ($controller instanceof ContainerAwareInterface) { 77 | $controller->setContainer($this->container); 78 | } 79 | return $controller; 80 | } 81 | } -------------------------------------------------------------------------------- /src/Jade/HttpKernel/HttpKernelProvider.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | namespace Jade\HttpKernel; 13 | 14 | use Jade\ContainerInterface; 15 | use Jade\EventProviderInterface; 16 | use Jade\Routing\Router; 17 | use Jade\ServiceProviderInterface; 18 | use Symfony\Component\EventDispatcher\EventDispatcherInterface; 19 | use Zend\HttpHandlerRunner\Emitter\SapiEmitter; 20 | use Zend\HttpHandlerRunner\Emitter\SapiStreamEmitter; 21 | 22 | class HttpKernelProvider implements ServiceProviderInterface, EventProviderInterface 23 | { 24 | public function register(ContainerInterface $container) 25 | { 26 | $container->add($this->getDefaults()); 27 | // 路由控制 28 | $container['router'] = function($c){ 29 | return new Router($c['route_collector']->getRoutes()); 30 | }; 31 | $container['router_listener'] = function($c){ 32 | return new RouterListener($c['router']); 33 | }; 34 | // http kernel 35 | $container['controller_resolver'] = function($c){ 36 | return new $c['http_kernel.controller_resolver_class']($c); 37 | }; 38 | $container['argument_resolver'] = function($c){ 39 | return new $c['http_kernel.argument_resolver_class']; 40 | }; 41 | // http response emitter 42 | $container['http_emitter'] = function(){ 43 | return new EmitterDecorator( 44 | new SapiEmitter(), 45 | new SapiStreamEmitter() 46 | ); 47 | }; 48 | $container['http_kernel'] = function($c){ 49 | return new HttpKernel( 50 | $c['event_dispatcher'], 51 | $c['controller_resolver'], 52 | $c['argument_resolver'], 53 | $c['middleware_pipeline'], 54 | $c['http_emitter'] 55 | ); 56 | }; 57 | } 58 | 59 | protected function getDefaults() 60 | { 61 | return [ 62 | 'http_kernel.controller_resolver_class' => ControllerResolver::class, 63 | 'http_kernel.argument_resolver_class' => ArgumentResolver::class 64 | ]; 65 | } 66 | 67 | public function subscribe(EventDispatcherInterface $eventDispatcher, ContainerInterface $container) 68 | { 69 | $eventDispatcher->addSubscriber($container['router_listener']); 70 | } 71 | } -------------------------------------------------------------------------------- /src/Jade/HttpKernel/ArgumentResolver.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | namespace Jade\HttpKernel; 13 | 14 | use Psr\Http\Message\ServerRequestInterface; 15 | 16 | class ArgumentResolver implements ArgumentResolverInterface 17 | { 18 | protected $defaults = []; 19 | 20 | public function __construct($defaults = []) 21 | { 22 | $this->defaults = $defaults; 23 | } 24 | 25 | /** 26 | * {@inheritdoc} 27 | */ 28 | public function getArguments(ServerRequestInterface $request, callable $controller) 29 | { 30 | $this->addDefault('request', $request); 31 | $providedArguments = array_merge( 32 | $this->defaults, 33 | $request->getAttribute('_route')->getArguments() 34 | ); 35 | $arguments = []; 36 | $parameters = $this->createArgumentMetadata($controller); 37 | foreach ($parameters as $parameter) { 38 | $name = $parameter->getName(); 39 | $arguments[$name] = isset($providedArguments[$name]) 40 | ? $providedArguments[$name] : ( 41 | $parameter->isOptional() ? $parameter->getDefaultValue() : null 42 | ); 43 | } 44 | return $arguments; 45 | } 46 | 47 | /** 48 | * 添加一个默认参数 49 | * 50 | * @param string $name 51 | * @param mixed $value 52 | */ 53 | public function addDefault($name, $value) 54 | { 55 | $this->defaults[$name] = $value; 56 | } 57 | 58 | /** 59 | * 设置默认 60 | * 61 | * @param array $defaults 62 | */ 63 | public function setDefaults(array $defaults): void 64 | { 65 | $this->defaults = $defaults; 66 | } 67 | 68 | /** 69 | * 分析控制器的参数 70 | * 71 | * @param callable $controller 72 | * @return \ReflectionParameter[] 73 | * @throws \ReflectionException 74 | */ 75 | protected function createArgumentMetadata($controller) 76 | { 77 | if (\is_array($controller)) { 78 | $reflection = new \ReflectionMethod($controller[0], $controller[1]); 79 | } elseif (\is_object($controller) && !$controller instanceof \Closure) { 80 | $reflection = (new \ReflectionObject($controller))->getMethod('__invoke'); 81 | } else { 82 | $reflection = new \ReflectionFunction($controller); 83 | } 84 | return $reflection->getParameters(); 85 | } 86 | } -------------------------------------------------------------------------------- /src/Jade/HttpKernel/RouterListener.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | namespace Jade\HttpKernel; 13 | 14 | use FastRoute\Dispatcher; 15 | use Jade\Exception\MethodNotAllowedHttpException; 16 | use Jade\Exception\NotFoundHttpException; 17 | use Jade\HttpKernel\Event\GetResponseEvent; 18 | use Jade\Routing\Route; 19 | use Jade\Routing\Router; 20 | use Symfony\Component\EventDispatcher\EventSubscriberInterface; 21 | 22 | class RouterListener implements EventSubscriberInterface 23 | { 24 | /** 25 | * @var Router 26 | */ 27 | protected $router; 28 | 29 | public function __construct(Router $router) 30 | { 31 | $this->router = $router; 32 | } 33 | 34 | public static function getSubscribedEvents() 35 | { 36 | return [ 37 | KernelEvents::MIDDLEWARE => 'onRequest' 38 | ]; 39 | } 40 | 41 | public function onRequest(GetResponseEvent $event) 42 | { 43 | $request = $event->getRequest(); 44 | $matches = $this->router->dispatch($request); 45 | switch ($matches[0]) { 46 | case Dispatcher::FOUND: 47 | $route = $this->router->searchRoute($matches[1]); 48 | $this->prepareRoute($route, $matches); 49 | // 记录当前路由到指定请求体里 50 | $request = $request->withAttribute('_route', $route); 51 | break; 52 | case Dispatcher::METHOD_NOT_ALLOWED: 53 | $message = sprintf('No route found for "%s %s": Method Not Allowed (Allow: %s)', 54 | $request->getMethod(), $request->getUri()->getPath(), 55 | implode(',', $matches[1]) 56 | ); 57 | throw new MethodNotAllowedHttpException($message); 58 | case Dispatcher::NOT_FOUND: 59 | $message = sprintf('No route found for "%s %s"', $request->getMethod(), $request->getUri()->getPath()); 60 | throw new NotFoundHttpException($message); 61 | } 62 | $event->setRequest($request); 63 | } 64 | 65 | /** 66 | * 预处理 route 67 | * 68 | * @param Route $route 69 | * @param array $matches 70 | */ 71 | protected function prepareRoute(Route $route, $matches) 72 | { 73 | $routeArguments = []; 74 | foreach ($matches[2] as $k => $v) { 75 | $routeArguments[$k] = urldecode($v); 76 | } 77 | $route->setArguments($routeArguments); 78 | } 79 | } -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |

2 | 3 | CakePHP 4 | 5 |

6 |

7 | 8 | Software License 9 | 10 | 11 | Build Status 12 | 13 | 14 | Coverage Status 15 | 16 | 17 | Scrutinizer 18 | 19 | 20 | Latest Stable Version 21 | 22 |

23 | 24 | Jade is a flexible PHP micro framework to develop web applications and APIs 25 | 26 | ## Installation 27 | 28 | The recommended way to install Jade is through Composer: 29 | 30 | ```bash 31 | $ composer require jadephp/jade 32 | ``` 33 | 34 | ## Quick Start 35 | 36 | ```php 37 | 38 | use Psr\Http\Message\ServerRequestInterface; 39 | use Psr\Http\Server\RequestHandlerInterface; 40 | use Zend\Diactoros\Response; 41 | 42 | // 1. Create App 43 | $app = new Jade\App(); 44 | 45 | // 2. Add routes 46 | $app->get('/ping', function(ServerRequestInterface $request){ 47 | return new Response\TextResponse('pong'); 48 | }); 49 | 50 | // 3. Add middlewares 51 | $app->pipe(function(ServerRequestInterface $request, RequestHandlerInterface $handler){ 52 | $response = $handler->handle($request); 53 | return $response->withHeader('X-Jade-Version', '0.0.1'); 54 | }); 55 | 56 | // 4. Listen and serve. 57 | $app->serve(); 58 | ``` 59 | 60 | The above code can create a simple heartbeat application. 61 | 62 | Test this with the built-in PHP server: 63 | 64 | ```bash 65 | php -S 127.0.0.1:8000 66 | ``` 67 | Use the browser open `http://127.0.0.1:8000/ping` 68 | 69 | ## Documentation 70 | 71 | Read the [documentation](./docs/index.md) for more information 72 | 73 | ## Tests 74 | 75 | To run the test suite, you need PHPUnit: 76 | 77 | ```bash 78 | $ phpunit 79 | ``` 80 | 81 | ## License 82 | 83 | Jade is licensed under The MIT license. See [MIT](https://opensource.org/licenses/MIT) 84 | 85 | -------------------------------------------------------------------------------- /tests/Routing/RouterTest.php: -------------------------------------------------------------------------------- 1 | assertSame($routes, $router->getRoutes()); 19 | } 20 | 21 | public function testSearchRoute() 22 | { 23 | $routes = new RouteCollection(); 24 | $router = new Router($routes); 25 | $route = new Route('route1', '/foo', function(){}, ['GET']); 26 | $routes->add($route); 27 | $this->assertSame($routes->search('route1'), $router->searchRoute('route1')); 28 | } 29 | 30 | public function testDispatcher() 31 | { 32 | $routes = new RouteCollection(); 33 | $router = new Router($routes); 34 | $this->assertInstanceOf(Dispatcher::class, $router->getDispatcher()); 35 | } 36 | 37 | protected function routerFactory() 38 | { 39 | $routes = new RouteCollection(); 40 | $route = new Route('route1', '/foo', function(){}, ['GET']); 41 | $route2 = new Route('route2', '/hello/{username}', function(){}, ['GET']); 42 | $route3 = new Route('route3', '/greet/{username}', function(){}, ['POST']); 43 | $routes->add($route); 44 | $routes->add($route2); 45 | $routes->add($route3); 46 | return new Router($routes); 47 | } 48 | 49 | public function testDispatch() 50 | { 51 | $router = $this->routerFactory(); 52 | $request = new ServerRequest([], [], '/foo', 'GET'); 53 | 54 | $routerInfo = $router->dispatch($request); 55 | $this->assertEquals(1, $routerInfo[0]); 56 | } 57 | 58 | public function testDispatchWithName() 59 | { 60 | $router = $this->routerFactory(); 61 | $request = new ServerRequest([], [], '/hello/Jade', 'GET'); 62 | 63 | $routerInfo = $router->dispatch($request); 64 | $this->assertEquals(1, $routerInfo[0]); 65 | $this->assertArrayHasKey('username', $routerInfo[2]); 66 | $this->assertEquals('Jade', $routerInfo[2]['username']); 67 | } 68 | 69 | public function testDispatchWithNonMatchedMethod() 70 | { 71 | $router = $this->routerFactory(); 72 | $request = new ServerRequest([], [], '/greet/Jade', 'GET'); 73 | 74 | $routerInfo = $router->dispatch($request); 75 | $this->assertEquals(2, $routerInfo[0]); 76 | $this->assertEquals(['POST'], $routerInfo[1]); 77 | } 78 | // 其它类型调度 fastroute 已经测试通过 79 | } -------------------------------------------------------------------------------- /src/Jade/Routing/RouteCollector.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | namespace Jade\Routing; 13 | 14 | class RouteCollector 15 | { 16 | /** 17 | * @var string 18 | */ 19 | protected $prefix; 20 | 21 | /** 22 | * @var Route[]|RouteCollection 23 | */ 24 | protected $routes = []; 25 | 26 | public function __construct($prefix = '') 27 | { 28 | $this->prefix = $prefix; 29 | $this->routes = new RouteCollection(); 30 | } 31 | 32 | /** 33 | * 获取路由集合 34 | * 35 | * @return RouteCollection 36 | */ 37 | public function getRoutes() 38 | { 39 | return $this->routes; 40 | } 41 | 42 | /** 43 | * 创建一条 http get 路由 44 | * 45 | * @param string $path 46 | * @param string|callable $action 47 | * @return Route 48 | */ 49 | public function get($path, $action) 50 | { 51 | return $this->map($path, $action, 'GET'); 52 | } 53 | 54 | /** 55 | * 创建一条 http post 路由 56 | * 57 | * @param string $path 58 | * @param string|callable $action 59 | * @return Route 60 | */ 61 | public function post($path, $action) 62 | { 63 | return $this->map($path, $action, 'POST'); 64 | } 65 | 66 | /** 67 | * 创建一条 http delete 路由 68 | * 69 | * @param string $path 70 | * @param string|callable $action 71 | * @return Route 72 | */ 73 | public function delete($path, $action) 74 | { 75 | return $this->map($path, $action, 'DELETE'); 76 | } 77 | 78 | /** 79 | * 创建一条 http put/patch 路由 80 | * 81 | * @param string $path 82 | * @param string|callable $action 83 | * @return Route 84 | */ 85 | public function put($path, $action) 86 | { 87 | return $this->map($path, $action, ['PUT', 'PATCH']); 88 | } 89 | 90 | /** 91 | * 创建一条 http options 路由 92 | * 93 | * @param string $path 94 | * @param string|callable $action 95 | * @return Route 96 | */ 97 | public function options($path, $action) 98 | { 99 | return $this->map($path, $action, 'OPTIONS'); 100 | } 101 | 102 | /** 103 | * 创建一条 http 请求路由 104 | * 105 | * @param string $path 106 | * @param string|callable $action 107 | * @param array|string $methods 108 | * @return Route 109 | */ 110 | public function map($path, $action, $methods = []) 111 | { 112 | $path = $this->prefix . $path; 113 | $methods = array_map('strtoupper', (array)$methods); 114 | $route = new Route(null, $path, $action, $methods); 115 | $this->getRoutes()->add($route); 116 | return $route; 117 | } 118 | 119 | public function any($path, $action) 120 | { 121 | return $this->map($path, $action); 122 | } 123 | 124 | /** 125 | * 创建一条通用前缀的路由组 126 | * 127 | * @param string $prefix 128 | * @param callable $callback 129 | */ 130 | public function prefix($prefix, callable $callback) 131 | { 132 | $collector = new RouteCollector($prefix); 133 | call_user_func($callback, $collector); 134 | $this->routes->merge($collector->getRoutes()); 135 | } 136 | } -------------------------------------------------------------------------------- /tests/AppTest.php: -------------------------------------------------------------------------------- 1 | assertInstanceOf(RouteCollection::class, $app->getRoutes()); 22 | } 23 | 24 | public function testGetRoute() 25 | { 26 | $path = '/foo'; 27 | $callable = function ($req, $res) { 28 | // Do something 29 | }; 30 | $app = new App(); 31 | $route = $app->get($path, $callable); 32 | 33 | $this->assertInstanceOf(Route::class, $route); 34 | $this->assertAttributeContains('GET', 'methods', $route); 35 | } 36 | 37 | public function testPostRoute() 38 | { 39 | $path = '/foo'; 40 | $callable = function ($req, $res) { 41 | // Do something 42 | }; 43 | $app = new App(); 44 | $route = $app->post($path, $callable); 45 | 46 | $this->assertInstanceOf(Route::class, $route); 47 | $this->assertAttributeContains('POST', 'methods', $route); 48 | } 49 | 50 | public function testPutRoute() 51 | { 52 | $path = '/foo'; 53 | $callable = function ($req, $res) { 54 | // Do something 55 | }; 56 | $app = new App(); 57 | $route = $app->put($path, $callable); 58 | 59 | $this->assertInstanceOf(Route::class, $route); 60 | $this->assertAttributeContains('PUT', 'methods', $route); 61 | $this->assertAttributeContains('PATCH', 'methods', $route); 62 | } 63 | 64 | public function testDeleteRoute() 65 | { 66 | $path = '/foo'; 67 | $callable = function ($req, $res) { 68 | // Do something 69 | }; 70 | $app = new App(); 71 | $route = $app->delete($path, $callable); 72 | 73 | $this->assertInstanceOf(Route::class, $route); 74 | $this->assertAttributeContains('DELETE', 'methods', $route); 75 | } 76 | 77 | public function testOptionsRoute() 78 | { 79 | $path = '/foo'; 80 | $callable = function ($req, $res) { 81 | // Do something 82 | }; 83 | $app = new App(); 84 | $route = $app->options($path, $callable); 85 | 86 | $this->assertInstanceOf(Route::class, $route); 87 | $this->assertAttributeContains('OPTIONS', 'methods', $route); 88 | } 89 | 90 | public function testContainer() 91 | { 92 | $app = new App(); 93 | $this->assertInstanceOf(ContainerInterface::class, $app->getContainer()); 94 | // 测试core provider 95 | $this->assertNotEmpty($app->getProviders()); 96 | $this->assertInstanceOf(ServiceProviderInterface::class, $app->getProviders()[0]); 97 | } 98 | 99 | public function testBaseService() 100 | { 101 | $app = new App(); 102 | $container = $app->getContainer(); 103 | $this->assertInstanceOf(EventDispatcherInterface::class, $container->get('event_dispatcher')); 104 | $this->assertInstanceOf(MiddlewarePipeInterface::class, $container->get('middleware_pipeline')); 105 | $this->assertInstanceOf(MiddlewareFactory::class, $container->get('middleware_factory')); 106 | } 107 | } 108 | -------------------------------------------------------------------------------- /src/Jade/Routing/Route.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | namespace Jade\Routing; 13 | 14 | class Route 15 | { 16 | /** 17 | * @var string 18 | */ 19 | protected $name; 20 | 21 | /** 22 | * @var string 23 | */ 24 | protected $pattern; 25 | 26 | /** 27 | * @var string|callable 28 | */ 29 | protected $action; 30 | 31 | /** 32 | * @var array 33 | */ 34 | protected $methods; 35 | 36 | /** 37 | * @var array 38 | */ 39 | protected $arguments = []; 40 | 41 | public function __construct($name, $pattern, $action, $methods = []) 42 | { 43 | $this->name = $name; 44 | $this->pattern = $pattern; 45 | $this->action = $action; 46 | $this->methods = $methods; 47 | } 48 | 49 | /** 50 | * 返回路由唯一名称 51 | * 52 | * @return string 53 | */ 54 | public function getName(): ?string 55 | { 56 | return $this->name; 57 | } 58 | 59 | /** 60 | * 设置唯一名称 61 | * 62 | * @param string $name 63 | */ 64 | public function setName(string $name): void 65 | { 66 | $this->name = $name; 67 | } 68 | 69 | /** 70 | * 返回路由 pattern 71 | * 72 | * @return string 73 | */ 74 | public function getPattern(): string 75 | { 76 | return $this->pattern; 77 | } 78 | 79 | /** 80 | * 设置路由 pattern 81 | * 82 | * @param string $pattern 83 | */ 84 | public function setPattern(string $pattern): void 85 | { 86 | $this->pattern = $pattern; 87 | } 88 | 89 | /** 90 | * 获取路由动作 91 | * 92 | * @return callable|string 93 | */ 94 | public function getAction() 95 | { 96 | return $this->action; 97 | } 98 | 99 | /** 100 | * 设置路由动作 101 | * 102 | * @param callable|string $action 103 | */ 104 | public function setAction($action): void 105 | { 106 | $this->action = $action; 107 | } 108 | 109 | /** 110 | * 获取路由限制的请求 111 | * 112 | * @return array 113 | */ 114 | public function getMethods(): array 115 | { 116 | return $this->methods; 117 | } 118 | 119 | /** 120 | * 设置路由限制请求 121 | * 122 | * @param array $methods 123 | */ 124 | public function setMethods(array $methods): void 125 | { 126 | $this->methods = $methods; 127 | } 128 | 129 | /** 130 | * 获取参数 131 | * 132 | * @return array 133 | */ 134 | public function getArguments(): array 135 | { 136 | return $this->arguments; 137 | } 138 | 139 | /** 140 | * 设置参数 141 | * 142 | * @param array $arguments 143 | */ 144 | public function setArguments(array $arguments): void 145 | { 146 | $this->arguments = $arguments; 147 | } 148 | 149 | /** 150 | * 设置单个参数 151 | * 152 | * @param string $name 153 | * @param mixed $value 154 | */ 155 | public function setArgument($name, $value) 156 | { 157 | $this->arguments[$name] = $value; 158 | } 159 | 160 | /** 161 | * 获取单个参数 162 | * 163 | * @param string $name 164 | * @param mixed $default 165 | * @return mixed 166 | */ 167 | public function getArgument($name, $default = null) 168 | { 169 | return $this->arguments[$name] ?? $default; 170 | } 171 | } -------------------------------------------------------------------------------- /src/Jade/Twig/TwigExtension.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | namespace Jade\Twig; 13 | 14 | use Jade\Routing\Router; 15 | use Psr\Http\Message\UriInterface; 16 | 17 | class TwigExtension extends \Twig\Extension\AbstractExtension 18 | { 19 | /** 20 | * @var Router 21 | */ 22 | private $router; 23 | /** 24 | * @var string|UriInterface 25 | */ 26 | private $uri; 27 | 28 | public function __construct($router, $uri) 29 | { 30 | $this->router = $router; 31 | $this->uri = $uri; 32 | } 33 | 34 | public function getName() 35 | { 36 | return 'slim'; 37 | } 38 | 39 | public function getFunctions() 40 | { 41 | return [ 42 | new \Twig\TwigFunction('path_for', array($this, 'pathFor')), 43 | new \Twig\TwigFunction('full_url_for', array($this, 'fullUrlFor')), 44 | new \Twig\TwigFunction('base_url', array($this, 'baseUrl')), 45 | new \Twig\TwigFunction('is_current_path', array($this, 'isCurrentPath')), 46 | new \Twig\TwigFunction('current_path', array($this, 'currentPath')), 47 | ]; 48 | } 49 | 50 | public function pathFor($name, $data = [], $queryParams = [], $appName = 'default') 51 | { 52 | return $this->router->pathFor($name, $data, $queryParams); 53 | } 54 | 55 | /** 56 | * Similar to pathFor but returns a fully qualified URL 57 | * 58 | * @param string $name The name of the route 59 | * @param array $data Route placeholders 60 | * @param array $queryParams 61 | * @param string $appName 62 | * @return string fully qualified URL 63 | */ 64 | public function fullUrlFor($name, $data = [], $queryParams = [], $appName = 'default') 65 | { 66 | $path = $this->pathFor($name, $data, $queryParams, $appName); 67 | /** @var Uri $uri */ 68 | if (is_string($this->uri)) { 69 | $uri = Uri::createFromString($this->uri); 70 | } else { 71 | $uri = $this->uri; 72 | } 73 | $scheme = $uri->getScheme(); 74 | $authority = $uri->getAuthority(); 75 | $host = ($scheme ? $scheme . ':' : '') 76 | . ($authority ? '//' . $authority : ''); 77 | return $host . $path; 78 | } 79 | 80 | public function baseUrl() 81 | { 82 | if (is_string($this->uri)) { 83 | return $this->uri; 84 | } 85 | if (method_exists($this->uri, 'getBaseUrl')) { 86 | return $this->uri->getBaseUrl(); 87 | } 88 | } 89 | 90 | public function isCurrentPath($name, $data = []) 91 | { 92 | return $this->router->pathFor($name, $data) === $this->uri->getBasePath() . '/' . ltrim($this->uri->getPath(), '/'); 93 | } 94 | 95 | /** 96 | * Returns current path on given URI. 97 | * 98 | * @param bool $withQueryString 99 | * @return string 100 | */ 101 | public function currentPath($withQueryString = false) 102 | { 103 | if (is_string($this->uri)) { 104 | return $this->uri; 105 | } 106 | $path = $this->uri->getBasePath() . '/' . ltrim($this->uri->getPath(), '/'); 107 | if ($withQueryString && '' !== $query = $this->uri->getQuery()) { 108 | $path .= '?' . $query; 109 | } 110 | return $path; 111 | } 112 | 113 | /** 114 | * Set the base url 115 | * 116 | * @param string|Slim\Http\Uri $baseUrl 117 | * @return void 118 | */ 119 | public function setBaseUrl($baseUrl) 120 | { 121 | $this->uri = $baseUrl; 122 | } 123 | } -------------------------------------------------------------------------------- /src/Jade/HttpKernel/HttpKernel.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | namespace Jade\HttpKernel; 13 | 14 | use Psr\Http\Message\ResponseInterface; 15 | use Psr\Http\Message\ServerRequestInterface; 16 | use Psr\Http\Server\RequestHandlerInterface; 17 | use Jade\HttpKernel\Event\FilterControllerEvent; 18 | use Jade\HttpKernel\Event\FilterResponseEvent; 19 | use Jade\HttpKernel\Event\GetResponseEvent; 20 | use Jade\HttpKernel\Event\PostResponseEvent; 21 | use Symfony\Component\EventDispatcher\EventDispatcherInterface; 22 | use Zend\HttpHandlerRunner\Emitter\EmitterInterface; 23 | use Zend\Stratigility\MiddlewarePipeInterface; 24 | 25 | final class HttpKernel implements RequestHandlerInterface 26 | { 27 | /** 28 | * @var EventDispatcherInterface 29 | */ 30 | protected $eventDispatcher; 31 | 32 | /** 33 | * @var MiddlewarePipeInterface 34 | */ 35 | protected $pipeline; 36 | 37 | /** 38 | * @var ControllerResolverInterface 39 | */ 40 | protected $controllerResolver; 41 | 42 | /** 43 | * @var ArgumentResolverInterface 44 | */ 45 | protected $argumentResolver; 46 | 47 | /** 48 | * @var EmitterInterface 49 | */ 50 | protected $emitter; 51 | 52 | public function __construct( 53 | EventDispatcherInterface $eventDispatcher, 54 | ControllerResolverInterface $controllerResolver, 55 | ArgumentResolverInterface $argumentResolver, 56 | MiddlewarePipeInterface $pipeline, 57 | EmitterInterface $emitter 58 | ){ 59 | $this->eventDispatcher = $eventDispatcher; 60 | $this->controllerResolver = $controllerResolver; 61 | $this->argumentResolver = $argumentResolver; 62 | $this->pipeline = $pipeline; 63 | $this->emitter = $emitter; 64 | } 65 | 66 | /** 67 | * {@inheritdoc} 68 | */ 69 | public function handle(ServerRequestInterface $request): ResponseInterface 70 | { 71 | // 1. 触发事件 72 | $event = new GetResponseEvent($this, $request); 73 | $this->eventDispatcher->dispatch($event, KernelEvents::REQUEST); 74 | // 2. 调度middleware 75 | return $this->pipeline->process($event->getRequest(), new CallableRequestHandler([$this, 'handleRequest'])); 76 | } 77 | 78 | /** 79 | * Emit response. 80 | * 81 | * @param ServerRequestInterface $request 82 | * @param ResponseInterface $response 83 | */ 84 | public function terminate(ServerRequestInterface $request, ResponseInterface $response) 85 | { 86 | $event = new PostResponseEvent($this, $request, $response); 87 | $this->eventDispatcher->dispatch($event, KernelEvents::TERMINATE); 88 | 89 | $this->emitter->emit($response); 90 | } 91 | 92 | /** 93 | * 处理请求 94 | * 95 | * @param ServerRequestInterface $request 96 | * @return ResponseInterface 97 | */ 98 | public function handleRequest(ServerRequestInterface $request): ?ResponseInterface 99 | { 100 | // 1. middleware 到达结束 101 | $event = new GetResponseEvent($this, $request); 102 | $this->eventDispatcher->dispatch($event, KernelEvents::MIDDLEWARE); 103 | $request = $event->getRequest(); 104 | 105 | if ($event->hasResponse()) { 106 | return $this->filterResponse($event->getResponse(), $request); 107 | } 108 | 109 | // 2. 获取控制器 110 | $controller = $this->controllerResolver->getController($request); 111 | $event = new FilterControllerEvent($this, $request, $controller); 112 | $this->eventDispatcher->dispatch($event, KernelEvents::CONTROLLER); 113 | 114 | $controller = $event->getController(); 115 | $response = call_user_func_array($controller, 116 | $this->argumentResolver->getArguments($request, $controller) 117 | ); 118 | 119 | // 3. 过滤响应体 120 | return $this->filterResponse($response, $request); 121 | } 122 | 123 | protected function filterResponse(ResponseInterface $response, ServerRequestInterface $request) 124 | { 125 | $event = new FilterResponseEvent($this, $request, $response); 126 | $this->eventDispatcher->dispatch($event, KernelEvents::RESPONSE); 127 | return $event->getResponse(); 128 | } 129 | } -------------------------------------------------------------------------------- /src/Jade/App.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | namespace Jade; 13 | 14 | use Jade\Routing\RouteCollector; 15 | use Psr\Container\ContainerInterface; 16 | use Psr\Http\Message\ResponseInterface; 17 | use Psr\Http\Message\ServerRequestInterface; 18 | use Psr\Http\Server\MiddlewareInterface; 19 | use Psr\Http\Server\RequestHandlerInterface; 20 | use Jade\HttpKernel\HttpKernelProvider; 21 | use Jade\Routing\RouteCollection as RouteCollection; 22 | use Jade\Routing\Route; 23 | use Jade\Middleware\RouteMiddleware; 24 | use Zend\Diactoros\ServerRequestFactory; 25 | 26 | class App extends RouteCollector implements RequestHandlerInterface 27 | { 28 | /** 29 | * 是否已经初始化 30 | * 31 | * @var bool 32 | */ 33 | protected $booted = false; 34 | 35 | /** 36 | * @var ContainerInterface 37 | */ 38 | protected $container; 39 | 40 | /** 41 | * @var array 42 | */ 43 | protected $providers; 44 | 45 | public function __construct(ContainerInterface $container = null) 46 | { 47 | if (null === $container) { 48 | $container = new Container(); 49 | } 50 | // 注册核心服务 51 | $this->container = $container; 52 | $this->container['app'] = $this; 53 | $this->register(new CoreServiceProvider()); 54 | parent::__construct(); 55 | } 56 | 57 | /** 58 | * 初始化启动工作 59 | */ 60 | public function boot() 61 | { 62 | if ($this->booted) { 63 | return; 64 | } 65 | $this->booted = true; 66 | } 67 | 68 | /** 69 | * {@inheritdoc} 70 | */ 71 | public function handle(ServerRequestInterface $request): ResponseInterface 72 | { 73 | // 启动应用 74 | $this->boot(); 75 | $this->register(new HttpKernelProvider()); 76 | // 请求转交给 http kernel 77 | return $this->container->get('http_kernel')->handle($request); 78 | } 79 | 80 | /** 81 | * 代理http kernel 82 | * 83 | * @param ServerRequestInterface $request 84 | * @param ResponseInterface $response 85 | */ 86 | public function terminate(ServerRequestInterface $request, ResponseInterface $response) 87 | { 88 | $this->container->get('http_kernel')->terminate($request, $response); 89 | } 90 | 91 | /** 92 | * 开启服务, 监听请求 93 | */ 94 | public function serve() 95 | { 96 | // 1. 创建请求 97 | $request = ServerRequestFactory::fromGlobals(); 98 | // 2. 处理请求 99 | $response = $this->handle($request); 100 | // 3. 输出响应 101 | $this->terminate($request, $response); 102 | } 103 | 104 | /** 105 | * 注册服务提供者 106 | * 107 | * @param object $provider 108 | * @param array $values 109 | */ 110 | public function register($provider, array $values = []) 111 | { 112 | // 注册服务 113 | if ($provider instanceof ServiceProviderInterface) { 114 | $provider->register($this->container); 115 | } 116 | // 注册事件 117 | if ($provider instanceof EventProviderInterface) { 118 | $provider->subscribe($this->container->get('event_dispatcher'), $this->container); 119 | } 120 | $this->container->merge($values); 121 | $this->providers[] = $provider; 122 | } 123 | 124 | /** 125 | * 获取服务容器 126 | * 127 | * @return Container 128 | */ 129 | public function getContainer(): Container 130 | { 131 | return $this->container; 132 | } 133 | 134 | /** 135 | * 添加一个 middleware 136 | * 137 | * @param string|MiddlewareInterface|callable $middleware 138 | * @param Route|null $route 绑定的路由 139 | */ 140 | public function pipe($middleware, Route $route = null) 141 | { 142 | $middleware = $this->container->get('middleware_factory')->create($middleware); 143 | if (null !== $route) { 144 | $middleware = new RouteMiddleware($route, $middleware); 145 | } 146 | $this->container->get('middleware_pipeline')->pipe( 147 | $middleware 148 | ); 149 | } 150 | 151 | /** 152 | * 返回全部的 provider 153 | * 154 | * @return array 155 | */ 156 | public function getProviders() 157 | { 158 | return $this->providers; 159 | } 160 | } -------------------------------------------------------------------------------- /src/Jade/Provider/DoctrineServiceProvider.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | namespace Jade\Provider; 13 | 14 | use Doctrine\DBAL\DriverManager; 15 | use Doctrine\DBAL\Configuration; 16 | use Doctrine\Common\EventManager; 17 | use Jade\Container; 18 | use Jade\ContainerInterface; 19 | use Jade\ServiceProviderInterface; 20 | use Symfony\Bridge\Doctrine\Logger\DbalLogger; 21 | 22 | /** 23 | * Doctrine DBAL Provider. 24 | * 25 | * @author Fabien Potencier 26 | */ 27 | class DoctrineServiceProvider implements ServiceProviderInterface 28 | { 29 | public function register(ContainerInterface $app) 30 | { 31 | $app['db.default_options'] = [ 32 | 'driver' => 'pdo_mysql', 33 | 'dbname' => null, 34 | 'host' => 'localhost', 35 | 'user' => 'root', 36 | 'password' => null, 37 | ]; 38 | 39 | $app['dbs.options.initializer'] = $app->protect(function () use ($app) { 40 | static $initialized = false; 41 | 42 | if ($initialized) { 43 | return; 44 | } 45 | 46 | $initialized = true; 47 | 48 | if (!isset($app['dbs.options'])) { 49 | $app['dbs.options'] = ['default' => isset($app['db.options']) ? $app['db.options'] : []]; 50 | } 51 | 52 | $tmp = $app['dbs.options']; 53 | foreach ($tmp as $name => &$options) { 54 | $options = array_replace($app['db.default_options'], $options); 55 | 56 | if (!isset($app['dbs.default'])) { 57 | $app['dbs.default'] = $name; 58 | } 59 | } 60 | $app['dbs.options'] = $tmp; 61 | }); 62 | 63 | $app['dbs'] = function ($app) { 64 | $app['dbs.options.initializer'](); 65 | 66 | $dbs = new Container(); 67 | foreach ($app['dbs.options'] as $name => $options) { 68 | if ($app['dbs.default'] === $name) { 69 | // we use shortcuts here in case the default has been overridden 70 | $config = $app['db.config']; 71 | $manager = $app['db.event_manager']; 72 | } else { 73 | $config = $app['dbs.config'][$name]; 74 | $manager = $app['dbs.event_manager'][$name]; 75 | } 76 | 77 | $dbs[$name] = function ($dbs) use ($options, $config, $manager) { 78 | return DriverManager::getConnection($options, $config, $manager); 79 | }; 80 | } 81 | 82 | return $dbs; 83 | }; 84 | 85 | $app['dbs.config'] = function ($app) { 86 | $app['dbs.options.initializer'](); 87 | 88 | $configs = new Container(); 89 | $addLogger = isset($app['logger']) && null !== $app['logger'] && class_exists('Symfony\Bridge\Doctrine\Logger\DbalLogger'); 90 | foreach ($app['dbs.options'] as $name => $options) { 91 | $configs[$name] = new Configuration(); 92 | if ($addLogger) { 93 | $configs[$name]->setSQLLogger(new DbalLogger($app['logger'], isset($app['stopwatch']) ? $app['stopwatch'] : null)); 94 | } 95 | } 96 | 97 | return $configs; 98 | }; 99 | 100 | $app['dbs.event_manager'] = function ($app) { 101 | $app['dbs.options.initializer'](); 102 | 103 | $managers = new Container(); 104 | foreach ($app['dbs.options'] as $name => $options) { 105 | $managers[$name] = new EventManager(); 106 | } 107 | 108 | return $managers; 109 | }; 110 | 111 | // shortcuts for the "first" DB 112 | $app['db'] = function ($app) { 113 | $dbs = $app['dbs']; 114 | 115 | return $dbs[$app['dbs.default']]; 116 | }; 117 | 118 | $app['db.config'] = function ($app) { 119 | $dbs = $app['dbs.config']; 120 | 121 | return $dbs[$app['dbs.default']]; 122 | }; 123 | 124 | $app['db.event_manager'] = function ($app) { 125 | $dbs = $app['dbs.event_manager']; 126 | 127 | return $dbs[$app['dbs.default']]; 128 | }; 129 | } 130 | } -------------------------------------------------------------------------------- /src/Jade/Routing/Router.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | namespace Jade\Routing; 13 | 14 | use FastRoute\Dispatcher; 15 | use FastRoute\RouteCollector; 16 | use FastRoute\RouteParser; 17 | use FastRoute\RouteParser\Std as StdParser; 18 | use Psr\Http\Message\ServerRequestInterface; 19 | 20 | class Router 21 | { 22 | /** 23 | * @var RouteCollection|Route[] 24 | */ 25 | protected $routes; 26 | 27 | /** 28 | * @var 29 | */ 30 | protected $routeParser; 31 | 32 | /** 33 | * @var Dispatcher 34 | */ 35 | protected $dispatcher; 36 | 37 | /** 38 | * 缓存路由文件,默认不缓存 39 | * 40 | * @var string|False 41 | */ 42 | protected $cacheFile = false; 43 | 44 | public function __construct(RouteCollection $routes, RouteParser $parser = null) 45 | { 46 | $this->routes = $routes; 47 | $this->routeParser = $parser ?: new StdParser(); 48 | } 49 | 50 | /** 51 | * 设置路由集合 52 | * 53 | * @param RouteCollection $routes 54 | */ 55 | public function setRoutes($routes): void 56 | { 57 | $this->routes = $routes; 58 | } 59 | 60 | /** 61 | * 返回路由集合 62 | * 63 | * @return RouteCollection 64 | */ 65 | public function getRoutes(): RouteCollection 66 | { 67 | return $this->routes; 68 | } 69 | 70 | /** 71 | * 搜索路由 72 | * 73 | * @param string $name 74 | * @return Route 75 | */ 76 | public function searchRoute($name) 77 | { 78 | return $this->routes->search($name); 79 | } 80 | 81 | /** 82 | * 调度请求 83 | * 84 | * @param ServerRequestInterface $request 85 | * @return array 86 | */ 87 | public function dispatch(ServerRequestInterface $request) 88 | { 89 | $uri = '/' . ltrim($request->getUri()->getPath(), '/'); 90 | return $this->getDispatcher()->dispatch( 91 | $request->getMethod(), 92 | $uri 93 | ); 94 | } 95 | 96 | /** 97 | * 设置自定义路由调度器 98 | * 99 | * @param Dispatcher $dispatcher 100 | */ 101 | public function setDispatcher(Dispatcher $dispatcher) 102 | { 103 | $this->dispatcher = $dispatcher; 104 | } 105 | 106 | /** 107 | * 获取路由调度器 108 | * 109 | * @return Dispatcher 110 | */ 111 | public function getDispatcher() 112 | { 113 | if ($this->dispatcher) { 114 | return $this->dispatcher; 115 | } 116 | return $this->dispatcher = $this->createDispatcher(); 117 | } 118 | 119 | /** 120 | * 设置路由缓存文件,为空表示禁用路由缓存 121 | * 122 | * @param string|false $cacheFile 123 | * 124 | * @return static 125 | * 126 | * @throws \InvalidArgumentException If cacheFile is not a string or not false 127 | * @throws \RuntimeException If cacheFile directory is not writable 128 | */ 129 | public function setCacheFile($cacheFile) 130 | { 131 | if (!is_string($cacheFile) && $cacheFile !== false) { 132 | throw new \InvalidArgumentException('Router cache file must be a string or false'); 133 | } 134 | if ($cacheFile && file_exists($cacheFile) && !is_readable($cacheFile)) { 135 | throw new \RuntimeException( 136 | sprintf('Router cache file `%s` is not readable', $cacheFile) 137 | ); 138 | } 139 | if ($cacheFile && !file_exists($cacheFile) && !is_writable(dirname($cacheFile))) { 140 | throw new \RuntimeException( 141 | sprintf('Router cache file directory `%s` is not writable', dirname($cacheFile)) 142 | ); 143 | } 144 | $this->cacheFile = $cacheFile; 145 | return $this; 146 | } 147 | 148 | /** 149 | * 创建路由调度 150 | * 151 | * @return Dispatcher 152 | */ 153 | protected function createDispatcher() 154 | { 155 | $routeDefinitionCallback = function (RouteCollector $r) { 156 | foreach ($this->routes as $route) { 157 | $r->addRoute($route->getMethods(), $route->getPattern(), $route->getName()); 158 | } 159 | }; 160 | if ($this->cacheFile) { 161 | $dispatcher = \FastRoute\cachedDispatcher($routeDefinitionCallback, [ 162 | 'routeParser' => $this->routeParser, 163 | 'cacheFile' => $this->cacheFile, 164 | ]); 165 | } else { 166 | $dispatcher = \FastRoute\simpleDispatcher($routeDefinitionCallback, [ 167 | 'routeParser' => $this->routeParser, 168 | ]); 169 | } 170 | return $dispatcher; 171 | } 172 | } -------------------------------------------------------------------------------- /src/Jade/Provider/DoctrineCacheServiceProvider.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | namespace Jade\Provider; 13 | 14 | use Doctrine\Common\Cache\ArrayCache; 15 | use Doctrine\Common\Cache\RedisCache; 16 | use Doctrine\Common\Cache\ApcuCache; 17 | use Doctrine\Common\Cache\XcacheCache; 18 | use Doctrine\Common\Cache\MemcachedCache; 19 | use Doctrine\Common\Cache\FilesystemCache; 20 | use Doctrine\Common\Cache\MongoDBCache; 21 | use Jade\Container; 22 | use Jade\ContainerInterface; 23 | use Jade\ServiceProviderInterface; 24 | 25 | class DoctrineCacheServiceProvider implements ServiceProviderInterface 26 | { 27 | public function register(ContainerInterface $container) 28 | { 29 | $container['cache.default_options'] = [ 30 | 'driver' => 'array', 31 | 'namespace' => null, 32 | ]; 33 | 34 | $container['caches.options.initializer'] = $container->protect(function () use ($container) { 35 | static $initialized = false; 36 | 37 | if ($initialized) { 38 | return; 39 | } 40 | 41 | $initialized = true; 42 | 43 | if (!isset($container['caches.options'])) { 44 | $container['caches.options'] = [ 45 | 'default' => $container['cache.options'] ?? [], 46 | ]; 47 | } 48 | 49 | $container['caches.options'] = array_map(function ($options) use ($container) { 50 | return array_replace($container['cache.default_options'], is_array($options) 51 | ? $options 52 | : ['driver' => $options] 53 | ); 54 | }, $container['caches.options']); 55 | 56 | if (!isset($container['caches.default'])) { 57 | $container['caches.default'] = array_keys( 58 | array_slice($container['caches.options'], 0, 1) 59 | )[0]; 60 | } 61 | }); 62 | 63 | $container['caches'] = function (ContainerInterface $container) { 64 | $container['caches.options.initializer'](); 65 | 66 | $caches = new Container(); 67 | foreach ($container['caches.options'] as $name => $options) { 68 | $caches[$name] = function () use ($container, $options) { 69 | $cache = $container['cache_factory']($options['driver'], $options); 70 | 71 | if (isset($options['namespace'])) { 72 | $cache->setNamespace($options['namespace']); 73 | } 74 | 75 | return $cache; 76 | }; 77 | } 78 | 79 | return $caches; 80 | }; 81 | 82 | $container['cache_factory.filesystem'] = $container->protect(function ($options) { 83 | if (empty($options['cache_dir']) 84 | || false === is_dir($options['cache_dir']) 85 | ) { 86 | throw new \InvalidArgumentException( 87 | 'You must specify "cache_dir" for Filesystem.' 88 | ); 89 | } 90 | 91 | return new FilesystemCache($options['cache_dir']); 92 | }); 93 | 94 | $container['cache_factory.array'] = $container->protect(function ($options) { 95 | return new ArrayCache(); 96 | }); 97 | 98 | $container['cache_factory.apcu'] = $container->protect(function ($options) { 99 | return new ApcuCache(); 100 | }); 101 | 102 | $container['cache_factory.mongodb'] = $container->protect(function ($options) { 103 | if (empty($options['server']) 104 | || empty($options['name']) 105 | || empty($options['collection']) 106 | ) { 107 | throw new \InvalidArgumentException( 108 | 'You must specify "server", "name" and "collection" for MongoDB.' 109 | ); 110 | } 111 | 112 | $client = new \MongoClient($options['server']); 113 | $db = new \MongoDB($client, $options['name']); 114 | $collection = new \MongoCollection($db, $options['collection']); 115 | 116 | return new MongoDBCache($collection); 117 | }); 118 | 119 | $container['cache_factory.redis'] = $container->protect(function ($options) { 120 | if (empty($options['host']) || empty($options['port'])) { 121 | throw new \InvalidArgumentException('You must specify "host" and "port" for Redis.'); 122 | } 123 | 124 | $redis = new \Redis(); 125 | $redis->connect($options['host'], $options['port']); 126 | 127 | if (isset($options['password'])) { 128 | $redis->auth($options['password']); 129 | } 130 | 131 | $cache = new RedisCache(); 132 | $cache->setRedis($redis); 133 | 134 | return $cache; 135 | }); 136 | 137 | $container['cache_factory.xcache'] = $container->protect(function ($options) { 138 | 139 | if (empty($options['host']) || empty($options['port'])) { 140 | throw new \InvalidArgumentException('You must specify "host" and "port" for Memcached.'); 141 | } 142 | 143 | $memcached = new \Memcached(); 144 | $memcached->addServer($options['host'], $options['port']); 145 | 146 | $cache = new MemcachedCache(); 147 | $cache->setMemcached($memcached); 148 | 149 | return $cache; 150 | }); 151 | 152 | $container['cache_factory.xcache'] = $container->protect(function ($options) { 153 | return new XcacheCache(); 154 | }); 155 | 156 | $container['cache_factory'] = $container->protect(function ($driver, $options) use ($container) { 157 | if (isset($container['cache_factory.' . $driver])) { 158 | return $container['cache_factory.' . $driver]($options); 159 | } 160 | 161 | throw new \RuntimeException(); 162 | }); 163 | 164 | // shortcuts for the "first" cache 165 | $container['cache'] = function (Container $container) { 166 | $caches = $container['caches']; 167 | 168 | return $caches[$container['caches.default']]; 169 | }; 170 | } 171 | } -------------------------------------------------------------------------------- /src/Jade/Twig/Twig.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | namespace Jade\Twig; 13 | 14 | use Psr\Http\Message\ResponseInterface; 15 | 16 | class Twig implements \ArrayAccess 17 | { 18 | /** 19 | * Twig loader 20 | * 21 | * @var \Twig\Loader\LoaderInterface 22 | */ 23 | protected $loader; 24 | /** 25 | * Twig environment 26 | * 27 | * @var \Twig\Environment 28 | */ 29 | protected $environment; 30 | /** 31 | * Default view variables 32 | * 33 | * @var array 34 | */ 35 | protected $defaultVariables = []; 36 | /******************************************************************************** 37 | * Constructors and service provider registration 38 | *******************************************************************************/ 39 | /** 40 | * Create new Twig view 41 | * 42 | * @param string|array $path Path(s) to templates directory 43 | * @param array $settings Twig environment settings 44 | */ 45 | public function __construct($path, $settings = []) 46 | { 47 | $this->loader = $this->createLoader(is_string($path) ? [$path] : $path); 48 | $this->environment = new \Twig\Environment($this->loader, $settings); 49 | } 50 | /******************************************************************************** 51 | * Methods 52 | *******************************************************************************/ 53 | /** 54 | * Proxy method to add an extension to the Twig environment 55 | * 56 | * @param \Twig\Extension\ExtensionInterface $extension A single extension instance or an array of instances 57 | */ 58 | public function addExtension(\Twig\Extension\ExtensionInterface $extension) 59 | { 60 | $this->environment->addExtension($extension); 61 | } 62 | /** 63 | * Fetch rendered template 64 | * 65 | * @param string $template Template pathname relative to templates directory 66 | * @param array $data Associative array of template variables 67 | * 68 | * @throws \Twig\Error\LoaderError When the template cannot be found 69 | * @throws \Twig\Error\SyntaxError When an error occurred during compilation 70 | * @throws \Twig\Error\RuntimeError When an error occurred during rendering 71 | * 72 | * @return string 73 | */ 74 | public function fetch($template, $data = []) 75 | { 76 | $data = array_merge($this->defaultVariables, $data); 77 | return $this->environment->render($template, $data); 78 | } 79 | /** 80 | * Fetch rendered block 81 | * 82 | * @param string $template Template pathname relative to templates directory 83 | * @param string $block Name of the block within the template 84 | * @param array $data Associative array of template variables 85 | * 86 | * @return string 87 | */ 88 | public function fetchBlock($template, $block, $data = []) 89 | { 90 | $data = array_merge($this->defaultVariables, $data); 91 | return $this->environment->loadTemplate($template)->renderBlock($block, $data); 92 | } 93 | /** 94 | * Fetch rendered string 95 | * 96 | * @param string $string String 97 | * @param array $data Associative array of template variables 98 | * 99 | * @return string 100 | */ 101 | public function fetchFromString($string ="", $data = []) 102 | { 103 | $data = array_merge($this->defaultVariables, $data); 104 | return $this->environment->createTemplate($string)->render($data); 105 | } 106 | /** 107 | * Output rendered template 108 | * 109 | * @param ResponseInterface $response 110 | * @param string $template Template pathname relative to templates directory 111 | * @param array $data Associative array of template variables 112 | * @return ResponseInterface 113 | */ 114 | public function render(ResponseInterface $response, $template, $data = []) 115 | { 116 | $response->getBody()->write($this->fetch($template, $data)); 117 | return $response; 118 | } 119 | /** 120 | * Create a loader with the given path 121 | * 122 | * @param array $paths 123 | * @return \Twig\Loader\FilesystemLoader 124 | */ 125 | private function createLoader(array $paths) 126 | { 127 | $loader = new \Twig\Loader\FilesystemLoader(); 128 | foreach ($paths as $namespace => $path) { 129 | if (is_string($namespace)) { 130 | $loader->setPaths($path, $namespace); 131 | } else { 132 | $loader->addPath($path); 133 | } 134 | } 135 | return $loader; 136 | } 137 | /******************************************************************************** 138 | * Accessors 139 | *******************************************************************************/ 140 | /** 141 | * Return Twig loader 142 | * 143 | * @return \Twig\Loader\LoaderInterface 144 | */ 145 | public function getLoader() 146 | { 147 | return $this->loader; 148 | } 149 | /** 150 | * Return Twig environment 151 | * 152 | * @return \Twig\Environment 153 | */ 154 | public function getEnvironment() 155 | { 156 | return $this->environment; 157 | } 158 | /******************************************************************************** 159 | * ArrayAccess interface 160 | *******************************************************************************/ 161 | /** 162 | * Does this collection have a given key? 163 | * 164 | * @param string $key The data key 165 | * 166 | * @return bool 167 | */ 168 | public function offsetExists($key) 169 | { 170 | return array_key_exists($key, $this->defaultVariables); 171 | } 172 | /** 173 | * Get collection item for key 174 | * 175 | * @param string $key The data key 176 | * 177 | * @return mixed The key's value, or the default value 178 | */ 179 | public function offsetGet($key) 180 | { 181 | return $this->defaultVariables[$key]; 182 | } 183 | /** 184 | * Set collection item 185 | * 186 | * @param string $key The data key 187 | * @param mixed $value The data value 188 | */ 189 | public function offsetSet($key, $value) 190 | { 191 | $this->defaultVariables[$key] = $value; 192 | } 193 | /** 194 | * Remove item from collection 195 | * 196 | * @param string $key The data key 197 | */ 198 | public function offsetUnset($key) 199 | { 200 | unset($this->defaultVariables[$key]); 201 | } 202 | /******************************************************************************** 203 | * Countable interface 204 | *******************************************************************************/ 205 | /** 206 | * Get number of items in collection 207 | * 208 | * @return int 209 | */ 210 | public function count() 211 | { 212 | return count($this->defaultVariables); 213 | } 214 | /******************************************************************************** 215 | * IteratorAggregate interface 216 | *******************************************************************************/ 217 | /** 218 | * Get collection iterator 219 | * 220 | * @return \ArrayIterator 221 | */ 222 | public function getIterator() 223 | { 224 | return new \ArrayIterator($this->defaultVariables); 225 | } 226 | } -------------------------------------------------------------------------------- /src/Jade/Provider/DoctrineOrmServiceProvider.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | namespace Jade\Provider; 13 | 14 | use Doctrine\Common\Persistence\Mapping\Driver\MappingDriverChain; 15 | use Doctrine\ORM\Configuration; 16 | use Doctrine\ORM\EntityManager; 17 | use Doctrine\ORM\Mapping\Driver\SimplifiedXmlDriver; 18 | use Doctrine\ORM\Mapping\Driver\SimplifiedYamlDriver; 19 | use Doctrine\ORM\Mapping\Driver\XmlDriver; 20 | use Doctrine\ORM\Mapping\Driver\YamlDriver; 21 | use Doctrine\ORM\Tools\Console\ConsoleRunner; 22 | use Doctrine\ORM\Tools\ResolveTargetEntityListener; 23 | use Jade\CommandProviderInterface; 24 | use Jade\Console\Application; 25 | use Jade\Container; 26 | use Jade\ContainerInterface; 27 | use Jade\ServiceProviderInterface; 28 | 29 | /** 30 | * Doctrine ORM Pimple Service Provider. 31 | * 32 | * @author Beau Simensen 33 | */ 34 | class DoctrineOrmServiceProvider implements ServiceProviderInterface, CommandProviderInterface 35 | { 36 | public function provide(Application $app, ContainerInterface $container) 37 | { 38 | ConsoleRunner::addCommands($app); 39 | $helperSet = $app->getHelperSet(); 40 | $doctrineHelperSet = ConsoleRunner::createHelperSet($container['orm']); 41 | foreach ($doctrineHelperSet as $alias =>$helper) { 42 | $helperSet->set($helper, $alias); 43 | } 44 | $app->setHelperSet($helperSet); 45 | } 46 | 47 | public function register(ContainerInterface $container) 48 | { 49 | if (!isset($container['dbs'])) { 50 | throw new \LogicException( 51 | 'You must register the DoctrineServiceProvider to use the DoctrineOrmServiceProvider.' 52 | ); 53 | } 54 | 55 | if (!isset($container['caches'])) { 56 | throw new \LogicException( 57 | 'You must register the DoctrineCacheServiceProvider to use the DoctrineOrmServiceProvider.' 58 | ); 59 | } 60 | 61 | // 初始值 62 | $container->add($this->getOrmDefaults()); 63 | 64 | $container['ems.options.initializer'] = $container->protect(function () use ($container) { 65 | static $initialized = false; 66 | 67 | if ($initialized) { 68 | return; 69 | } 70 | 71 | $initialized = true; 72 | 73 | if (!isset($container['ems.options'])) { 74 | $container['ems.options'] = [ 75 | 'default' => $container['orm.options'] ?? [], 76 | ]; 77 | } 78 | 79 | $container['ems.options'] = array_map(function ($options) use ($container) { 80 | return array_replace($container['orm.default_options'], $options); 81 | }, $container['ems.options']); 82 | 83 | if (!isset($container['ems.default'])) { 84 | $container['ems.default'] = array_keys( 85 | array_slice($container['ems.options'], 0, 1) 86 | )[0]; 87 | } 88 | }); 89 | 90 | $container['ems'] = function (Container $container) { 91 | $container['ems.options.initializer'](); 92 | 93 | $ems = new Container(); 94 | foreach ($container['ems.options'] as $name => $options) { 95 | $config = $container['ems.default'] === $name 96 | ? $container['orm.config'] 97 | : $container['ems.config'][$name]; 98 | 99 | $connection = $container['dbs'][$options['connection']]; 100 | $manager = $container['dbs.event_manager'][$options['connection']]; 101 | 102 | if ($targetEntities = $options['resolve_target_entities'] ?? []) { 103 | $manager->addEventSubscriber( 104 | $container['orm.resolve_target_entity']($targetEntities) 105 | ); 106 | } 107 | 108 | $ems[$name] = function () use ($connection, $config, $manager) { 109 | return EntityManager::create($connection, $config, $manager); 110 | }; 111 | } 112 | 113 | return $ems; 114 | }; 115 | 116 | $container['ems.config'] = function (Container $container) { 117 | $container['ems.options.initializer'](); 118 | 119 | $configs = new Container(); 120 | foreach ($container['ems.options'] as $name => $options) { 121 | $config = new Configuration(); 122 | $config->setProxyDir($container['orm.proxy_dir']); 123 | $config->setProxyNamespace($container['orm.proxy_namespace']); 124 | $config->setAutoGenerateProxyClasses($container['orm.auto_generate_proxy_classes']); 125 | $config->setCustomStringFunctions($container['orm.custom_functions_string']); 126 | $config->setCustomNumericFunctions($container['orm.custom_functions_numeric']); 127 | $config->setCustomDatetimeFunctions($container['orm.custom_functions_datetime']); 128 | $config->setMetadataCacheImpl($container['orm.cache.factory']('metadata', $options)); 129 | $config->setQueryCacheImpl($container['orm.cache.factory']('query', $options)); 130 | $config->setResultCacheImpl($container['orm.cache.factory']('result', $options)); 131 | $config->setMetadataDriverImpl($container['orm.mapping.chain']($config, $options['mappings'])); 132 | 133 | $configs[$name] = $config; 134 | } 135 | 136 | return $configs; 137 | }; 138 | 139 | $container['orm.cache.factory'] = $container->protect(function ($type, $options) use ($container) { 140 | $type = $type . '_cache_driver'; 141 | 142 | $options[$type] = $options[$type] ?? 'array'; 143 | 144 | if (!is_array($options[$type])) { 145 | $options[$type] = [ 146 | 'driver' => $options[$type], 147 | ]; 148 | } 149 | 150 | $driver = $options[$type]['driver']; 151 | $namespace = $options[$type]['namespace'] ?? null; 152 | 153 | $cache = $container['cache_factory']($driver, $options); 154 | $cache->setNamespace($namespace); 155 | 156 | return $cache; 157 | }); 158 | 159 | $container['orm.mapping.chain'] = $container->protect(function (Configuration $config, array $mappings) { 160 | $chain = new MappingDriverChain(); 161 | 162 | foreach ($mappings as $mapping) { 163 | if (!is_array($mapping)) { 164 | throw new \InvalidArgumentException(); 165 | } 166 | 167 | $path = $mapping['path']; 168 | $namespace = $mapping['namespace']; 169 | 170 | switch ($mapping['type']) { 171 | case 'annotation': 172 | $annotationDriver = $config->newDefaultAnnotationDriver( 173 | $path, 174 | $mapping['use_simple_annotation_reader'] ?? true 175 | ); 176 | $chain->addDriver($annotationDriver, $namespace); 177 | break; 178 | case 'yml': 179 | $chain->addDriver(new YamlDriver($path), $namespace); 180 | break; 181 | case 'simple_yml': 182 | $driver = new SimplifiedYamlDriver([$path => $namespace]); 183 | $chain->addDriver($driver, $namespace); 184 | break; 185 | case 'xml': 186 | $chain->addDriver(new XmlDriver($path), $namespace); 187 | break; 188 | case 'simple_xml': 189 | $driver = new SimplifiedXmlDriver([$path => $namespace]); 190 | $chain->addDriver($driver, $namespace); 191 | break; 192 | default: 193 | throw new \InvalidArgumentException(); 194 | break; 195 | } 196 | } 197 | 198 | return $chain; 199 | }); 200 | 201 | $container['orm.resolve_target_entity'] = $container->protect(function (array $targetEntities) { 202 | $rtel = new ResolveTargetEntityListener(); 203 | 204 | foreach ($targetEntities as $originalEntity => $newEntity) { 205 | $rtel->addResolveTargetEntity($originalEntity, $newEntity, []); 206 | } 207 | 208 | return $rtel; 209 | }); 210 | 211 | // shortcuts for the "first" ORM 212 | $container['orm'] = function (Container $container) { 213 | $ems = $container['ems']; 214 | 215 | return $ems[$container['ems.default']]; 216 | }; 217 | 218 | $container['orm.config'] = function (Container $container) { 219 | $ems = $container['ems.config']; 220 | 221 | return $ems[$container['ems.default']]; 222 | }; 223 | } 224 | 225 | protected function getOrmDefaults() 226 | { 227 | return [ 228 | 'orm.proxy_dir' => null, 229 | 'orm.proxy_namespace' => 'Proxy', 230 | 'orm.auto_generate_proxy_classes' => true, 231 | 'orm.custom_functions_string' => [], 232 | 'orm.custom_functions_numeric' => [], 233 | 'orm.custom_functions_datetime' => [], 234 | 'orm.default_options' => [ 235 | 'connection' => 'default', 236 | 'mappings' => [], 237 | ] 238 | ]; 239 | } 240 | } --------------------------------------------------------------------------------