├── .gitignore
├── LICENSE
├── README.md
├── composer.json
├── composer.lock
├── phpunit.xml.dist
├── src
├── Configuration.php
├── Container.php
├── Controller.php
├── Database
│ ├── Connection.php
│ └── Model.php
├── Events
│ ├── PreControllerEvent.php
│ ├── RequestEvent.php
│ └── ResponseEvent.php
├── Framework.php
├── Helpers.php
├── Http
│ ├── EventSubscriber
│ │ └── RequestSubscriber.php
│ ├── Exception
│ │ └── InvalidCsrfTokenException.php
│ ├── Middleware
│ │ ├── AjaxMiddleware.php
│ │ ├── CsrfMiddleware.php
│ │ ├── MiddlewareInterface.php
│ │ └── ValidationMiddleware.php
│ ├── Request.php
│ ├── RequestHandler.php
│ ├── RequestInterface.php
│ ├── Response.php
│ ├── ResponseInterface.php
│ ├── ResponseIterator.php
│ └── Security.php
├── Logger.php
├── Resources
│ └── Views
│ │ ├── About.php
│ │ └── Error.php
├── Routing
│ ├── Command.php
│ ├── Exception
│ │ └── RouteNotFoundException.php
│ ├── Route.php
│ ├── RouteBuilder.php
│ ├── RouteBuilderInterface.php
│ └── RouteParam.php
├── Templating
│ ├── Driver
│ │ ├── BladeDriver.php
│ │ ├── PlainPhpDriver.php
│ │ ├── TemplateDriverInterface.php
│ │ └── TwigDriver.php
│ └── Template.php
├── UtilsTrait.php
└── Validator
│ ├── Exception
│ └── ValidationConstraintException.php
│ └── Validator.php
└── tests
├── ConfigurationTest.php
├── ContainerTest.php
├── FrameworkTest.php
└── assets
└── .env
/.gitignore:
--------------------------------------------------------------------------------
1 | /vendor/
2 | .buildpath
3 | .project
4 | .settings/org.eclipse.wst.validation.prefs
5 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2017 Ingenia Software
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | WARNING: Under development!
6 |
7 | **Luthier Framework** is a versatile PHP micro-framework for build APIs and small websites quickly. When we say "micro" we mean REALLY micro: in fact, only Composer and a single .php file is required to start.
8 |
9 | ### Features
10 |
11 | * Based on the Symfony components
12 | * Easy to learn and extend
13 | * Powerful and flexible router with middleware support
14 | * CSRF protection
15 | * JSON and XML response helpers
16 | * Validator with translated error messages
17 | * Dependency Injection container
18 | * Command Line Interface command creation
19 | * Built-in plain PHP template engine with Twig and Blade integration
20 |
21 | ### Requirements
22 |
23 | * PHP >= 7.1.8
24 | * Composer
25 |
26 | ### Installation
27 |
28 | Get Luthier Framework with composer:
29 |
30 | ```
31 | composer require luthier/framework
32 | ```
33 |
34 | ### Usage
35 |
36 | Basic example:
37 |
38 | ```php
39 | get('/', function(){
47 | $this->response->write("Hello world!");
48 | });
49 |
50 | $app->group('api', function(){
51 |
52 | $this->get('/', function(){
53 | json_response(['message' => 'Welcome to Luthier Framework!']);
54 | });
55 | $this->get('about', function(){
56 | json_response(['version' => Luthier\Framework::VERSION]);
57 | });
58 |
59 | });
60 |
61 | $app->run();
62 | ```
63 |
64 | Defining routes:
65 |
66 | ```php
67 | $app->get('foo/', function(){
68 | // Default template engine (will search for /foo.php file)
69 | view('foo');
70 | });
71 |
72 | $app->post('bar/', function(){
73 | view('bar');
74 | });
75 |
76 | $app->match(['get','post'], 'baz/', function(){
77 | view('baz');
78 | });
79 | ```
80 |
81 | Router parameters:
82 |
83 | ```php
84 | $app->get('hello/{name}', function($name){
85 | $this->response->write("Hello $name!");
86 | });
87 |
88 | // Optional parameters
89 |
90 | $app->get('about/{category?}', function($category = 'animals'){
91 | $this->response->write("Category: category");
92 | });
93 |
94 | // Regex parameters
95 |
96 | $app->get('website/{((en|es|fr)):lang}', function($lang){
97 | $this->response->write($lang);
98 | });
99 | ```
100 |
101 | Route middleware:
102 |
103 | ```php
104 | // Global middleware:
105 |
106 | $app->middleware(function($request, $response, $next){
107 | $response->write('Global
');
108 | $next($request, $response);
109 | });
110 |
111 | // Global middleware (but not assigned to any route yet)
112 |
113 | $app->middleware('test', function($request, $response, $next){
114 | $response->write('Before route
');
115 | $next($request, $response);
116 | $response->write('After route
');
117 | });
118 |
119 | $this->get('/', function(){
120 | $this->response->write('Route
')
121 | })->middleware('test'); // <- assign the 'test' middleware to this route
122 |
123 | ```
124 |
125 | ### Documentation
126 |
127 | Coming soon!
128 |
129 | ### Related projects
130 |
131 | * [Luthier CI](https://github.com/ingeniasoftware/luthier-ci): Improved routing, middleware support, authentication tools and more for CodeIgniter 3 framework
132 | * [SimpleDocs](https://github.com/ingeniasoftware/simpledocs): Dynamic documentation library for PHP which uses Markdown files
133 |
134 | ### Donate
135 |
136 | If you love our work, consider support us on [Patreon](https://patreon.com/ingenia)
137 |
--------------------------------------------------------------------------------
/composer.json:
--------------------------------------------------------------------------------
1 | {
2 | "name" : "luthier/framework",
3 | "type" : "library",
4 | "description" : "Versatile PHP micro-framework for build APIs and websites quickly",
5 | "license" : "MIT",
6 | "homepage" : "https://luthier.ingenia.me/framework/en/",
7 | "authors" : [{
8 | "name" : "Anderson Salas",
9 | "email" : "anderson@ingenia.me",
10 | "homepage" : "https://ingenia.me",
11 | "role" : "Lead developer"
12 | }
13 | ],
14 | "keywords" : [
15 | "framework",
16 | "micro-framework",
17 | "routing",
18 | "middleware",
19 | "dependency-injection",
20 | "luthier"
21 | ],
22 | "minimum-stability" : "stable",
23 | "require" : {
24 | "php" : ">=7.1.8",
25 | "symfony/http-foundation" : "^4.0",
26 | "symfony/http-kernel" : "^4.0",
27 | "symfony/routing" : "^4.0",
28 | "symfony/dotenv" : "^4.0",
29 | "symfony/event-dispatcher" : "^4.0",
30 | "symfony/console" : "^4.0",
31 | "symfony/validator" : "^4.0",
32 | "symfony/translation" : "^4.0",
33 | "pimple/pimple" : "~3.0",
34 | "filp/whoops" : "^2.2",
35 | "monolog/monolog" : "~1.0",
36 | "envms/fluentpdo" : "^1.1",
37 | "symfony/config" : "~3.4|~4.0",
38 | "spatie/array-to-xml" : "~2.0"
39 | },
40 | "require-dev" : {
41 | "phpunit/phpunit" : "^7",
42 | "twig/twig" : "^2.0",
43 | "illuminate/view" : "~5.0",
44 | "league/plates" : "~3.0"
45 | },
46 | "support" : {
47 | "email" : "anderson@ingenia.me"
48 | },
49 | "autoload" : {
50 | "psr-4" : {
51 | "Luthier\\" : "src/"
52 | }
53 | }
54 | }
--------------------------------------------------------------------------------
/phpunit.xml.dist:
--------------------------------------------------------------------------------
1 |
2 |
9 |
10 |
11 |
12 |
13 |
14 |
15 | tests
16 |
17 |
18 |
19 |
20 | ./src
21 |
22 |
23 |
24 |
--------------------------------------------------------------------------------
/src/Configuration.php:
--------------------------------------------------------------------------------
1 |
24 | */
25 | class Configuration
26 | {
27 |
28 | /**
29 | * @var array
30 | */
31 | protected $config;
32 |
33 | /**
34 | * @var bool
35 | */
36 | protected $envLoaded = false;
37 |
38 | /**
39 | * @var string
40 | */
41 | protected $envFolder;
42 |
43 | /**
44 | * The default application configuration
45 | *
46 | * @var array
47 | */
48 | protected static $defaultConfig = [
49 | // General configuration
50 | 'APP_ENV' => 'development',
51 | 'APP_NAME' => 'Luthier',
52 | 'APP_URL' => null,
53 | 'APP_LOG' => null,
54 | 'APP_CACHE' => null,
55 | 'APP_PATH' => null,
56 | 'APP_LANG' => 'en',
57 | // Session & Cookiesconfiguration
58 | 'SESSION_NAME' => 'luthier_session',
59 | // Database configuration
60 | 'DB_TYPE' => 'mysql',
61 | 'DB_HOST' => 'localhost',
62 | 'DB_USER' => 'root',
63 | 'DB_PASS' => null,
64 | 'DB_NAME' => null,
65 | 'DB_MDNS' => null,
66 | // Template configuration
67 | 'TEMPLATE_DRIVER' => 'default',
68 | 'TEMPLATE_DIR' => null,
69 | // CSRF protection configuration
70 | 'CSRF_TOKEN_NAME' => null,
71 | 'CSRF_TOKEN_LIFETIME' => 3600,
72 | 'CSRF_TOKEN_COOKIE_DOMAIN' => null,
73 | 'CSRF_TOKEN_COOKIE_PATH' => null,
74 | 'CSRF_TOKEN_LENGTH' => 16
75 | ];
76 |
77 | /**
78 | * @param array $config Application configuration array
79 | * @param string $envFolder Application .env file path
80 | */
81 | public function __construct(?array $config = [], ?string $envFolder = null)
82 | {
83 | $this->config = $config ?? [];
84 | $this->envFolder = $envFolder;
85 |
86 | if($envFolder !== null)
87 | $this->fetchEnvFile();
88 | }
89 |
90 | /**
91 | * Parses the provided application configuration
92 | *
93 | * The .env file configuration has more precedence that the application configuration
94 | * array, so any configuration provided by the first source will no be overwritten by
95 | * the second.
96 | *
97 | * Keep this in mind when you use both an .env file and a configuration array
98 | * in your application.
99 | *
100 | * @return array
101 | */
102 | public function parse()
103 | {
104 | $config = [];
105 |
106 | // Failsafe base configuration
107 | foreach (self::$defaultConfig as $name => $default) {
108 | $config[$name] = $this->getConfigValue($name, $default);
109 | }
110 |
111 | // All other configuration
112 | foreach ($this->config as $name => $value) {
113 | if (! isset($config[$name])) {
114 | $config[$name] = $value;
115 | }
116 | }
117 |
118 | $this->parsed = $config;
119 |
120 | return $config;
121 | }
122 |
123 | /**
124 | * @return array
125 | */
126 | public static function getDefaultConfig()
127 | {
128 | return self::$defaultConfig;
129 | }
130 |
131 | /**
132 | * @return array
133 | */
134 | public function getConfig()
135 | {
136 | return $this->config;
137 | }
138 |
139 | /**
140 | * @return string
141 | */
142 | public function getEnvFolder()
143 | {
144 | return $this->envFolder;
145 | }
146 |
147 | /**
148 | * @param array $config
149 | *
150 | * @return self
151 | */
152 | public function setConfig(array $config)
153 | {
154 | $this->config = $config;
155 | return $this;
156 | }
157 |
158 | /**
159 | * @param string $envFolder
160 | *
161 | * @return self
162 | */
163 | public function setEnvFolder(string $envFolder)
164 | {
165 | $this->envFolder = $envFolder;
166 | $this->fetchEnvFile();
167 | return $this;
168 | }
169 |
170 | private function fetchEnvFile()
171 | {
172 | if($this->envLoaded)
173 | return;
174 |
175 | if ($this->envFolder !== NULL) {
176 | try {
177 | (new Dotenv())->load(($this->envFolder !== NULL ? $this->envFolder . '/' : '') . '.env');
178 | } catch (PathException $e) {
179 | throw new \Exception('Unable to find your application .env file. Does the file exists?');
180 | } catch (\Exception $e) {
181 | throw new \Exception('Unable to parse your application .env file');
182 | }
183 | }
184 |
185 | $this->envLoaded = true;
186 | }
187 |
188 | /**
189 | * @param string $name
190 | * @param mixed $default
191 | *
192 | * @return mixed
193 | */
194 | public function getConfigValue(string $name, $default = null)
195 | {
196 | $this->fetchEnvFile();
197 |
198 | if ($this->envFolder !== NULL && getenv($name) !== FALSE) {
199 | // Empty strings are considered NULL
200 | return !empty(getenv($name)) ? getEnv($name) : null;
201 | } else if (isset($this->config[$name])) {
202 | return $this->config[$name];
203 | } else {
204 | return $default;
205 | }
206 | }
207 | }
--------------------------------------------------------------------------------
/src/Container.php:
--------------------------------------------------------------------------------
1 |
28 | */
29 | class Container
30 | {
31 |
32 | /**
33 | * @var \Pimple\Container
34 | */
35 | protected $container;
36 |
37 | /**
38 | * @var array
39 | */
40 | protected $publicAliases = [];
41 |
42 | /**
43 | * @var array
44 | */
45 | protected $privateAliases = [];
46 |
47 | /**
48 | * Gets the default container
49 | *
50 | * @var array
51 | */
52 | protected static $defaultContainer = [
53 | // (name) => [ (type), (class), (serviceLocatorAliases) ]
54 | 'router' => [
55 | 'service',
56 | \Luthier\Routing\RouteBuilder::class,
57 | []
58 | ],
59 | 'request_handler' => [
60 | 'service',
61 | \Luthier\Http\RequestHandler::class,
62 | [
63 | '@PRIVATE_SERVICES'
64 | ]
65 | ],
66 | 'dispatcher' => [
67 | 'service',
68 | \Symfony\Component\EventDispatcher\EventDispatcher::class,
69 | []
70 | ],
71 | 'request' => [
72 | 'service',
73 | \Luthier\Http\Request::class,
74 | []
75 | ],
76 | 'response' => [
77 | 'service',
78 | \Luthier\Http\Response::class,
79 | []
80 | ],
81 | 'logger' => [
82 | 'service',
83 | \Luthier\Logger::class,
84 | []
85 | ],
86 | 'database' => [
87 | 'service',
88 | \Luthier\Database\Connection::class,
89 | []
90 | ],
91 | 'template' => [
92 | 'service',
93 | \Luthier\Templating\Template::class,
94 | []
95 | ],
96 | 'validator' => [
97 | 'service',
98 | \Luthier\Validator\Validator::class,
99 | []
100 | ],
101 | 'security' => [
102 | 'service',
103 | \Luthier\Http\Security::class,
104 | []
105 | ],
106 | ];
107 |
108 | public function __construct()
109 | {
110 | $this->container = new PimpleContainer();
111 | }
112 |
113 | /**
114 | * Gets the default Luthier Framework container
115 | *
116 | * @return array
117 | */
118 | public static function getDefaultContainer()
119 | {
120 | return self::$defaultContainer;
121 | }
122 |
123 | /**
124 | * Gets a service callback from a string (in this case, a fully qualified class name)
125 | *
126 | * @param string $name The service class name
127 | */
128 | private function getServiceCallback(string $service, array $locatorAliases = [])
129 | {
130 | $locatorAliases = array_merge(
131 | array_keys(Configuration::getDefaultConfig()),
132 | array_keys(self::getDefaultContainer()),
133 | ['translator'],
134 | $locatorAliases,
135 | $this->publicAliases
136 | );
137 |
138 | return function ($container) use ($service, $locatorAliases) {
139 | return new $service(new ServiceLocator($container, $locatorAliases));
140 | };
141 | }
142 |
143 | /**
144 | * Determines if a service is public or private based on the provided
145 | * name
146 | *
147 | * @param string $name
148 | * @param mixed $class
149 | * @param bool $isPublic
150 | */
151 | private function parseItem(&$name, $class, &$isPublic)
152 | {
153 | if (substr($name, 0, 1) == '.') {
154 | $name = substr($name, 1);
155 | $isPublic = false;
156 | }
157 |
158 | if ($isPublic && ! in_array($name, $this->publicAliases)) {
159 | $this->publicAliases[] = $name;
160 | } else {
161 | if ($class !== null && is_string($class)) {
162 | $this->privateAliases[] = [
163 | $name,
164 | $class
165 | ];
166 | }
167 | }
168 | }
169 |
170 | /**
171 | * Registers a new service in the container
172 | *
173 | * @param string $name Service name
174 | * @param callable|string $service Service callback
175 | * @param array $locatorAliases Array of service aliases of the service locator
176 | * @param bool $isPublic Set the service as public (will be available globally in the application)
177 | *
178 | * @return self
179 | */
180 | public function service(string $name, $service, array $locatorAliases = [], bool $isPublic = true)
181 | {
182 | $this->parseItem($name, $service, $isPublic);
183 | $this->container[$name] = is_string($service)
184 | ? $this->getServiceCallback($service, $locatorAliases)
185 | : $service;
186 |
187 | return $this;
188 | }
189 |
190 | /**
191 | * Registers a new factory in the container
192 | *
193 | * @param string $name Factory name
194 | * @param string|callable $factory Factory callback
195 | * @param array $locatorAliases Array of service aliases of the service locator
196 | *
197 | * @return self
198 | */
199 | public function factory(string $name, $factory, array $locatorAliases = [])
200 | {
201 | $this->parseItem($name, $factory);
202 | $this->container[$name] = $this->container->factory(
203 | is_string($factory)
204 | ? $this->getServiceCallback($factory, $locatorAliases)
205 | : $factory
206 | );
207 |
208 | return $this;
209 | }
210 |
211 | /**
212 | * Registers a new parameter in the container
213 | *
214 | * All parameters are PUBLIC and their names will be converted to UPPERCASE
215 | *
216 | * @param string $name Parameter name
217 | * @param mixed $value Parameter value
218 | * @param bool $private Mark the parameter as private
219 | *
220 | * @return self
221 | */
222 | public function parameter(string $name, $value)
223 | {
224 | $name = strtoupper($name);
225 |
226 | if (! in_array($name, $this->publicAliases)) {
227 | $this->publicAliases[] = $name;
228 | }
229 |
230 | $this->container[$name] = is_callable($value)
231 | ? $this->container->protect($value)
232 | : $value;
233 |
234 | return $this;
235 | }
236 |
237 | /**
238 | * @return array
239 | */
240 | public function getPrivateAliases()
241 | {
242 | return $this->privateAliases;
243 | }
244 |
245 | /**
246 | * Gets a service/parameter from the container by its id
247 | *
248 | * @param string $id
249 | *
250 | * @return mixed
251 | */
252 | public function get($id)
253 | {
254 | if ($this->has($id)) {
255 | return $this->container[$id];
256 | } else {
257 | throw new \Exception("The '$id' service/parameter is not defined in the container");
258 | }
259 | }
260 |
261 | /**
262 | * Checks the existence of the provided service/parameter id
263 | *
264 | * @param string $id
265 | *
266 | * @return bool
267 | */
268 | public function has($id)
269 | {
270 | return isset($this->container[$id]);
271 | }
272 | }
--------------------------------------------------------------------------------
/src/Controller.php:
--------------------------------------------------------------------------------
1 |
24 | */
25 | class Controller
26 | {
27 |
28 | /**
29 | * @var \Pimple\Psr11\Container
30 | */
31 | protected $container;
32 |
33 | /**
34 | * @var \Luthier\Http\Request
35 | */
36 | protected $request;
37 |
38 | /**
39 | * @var \Luthier\Http\Response
40 | */
41 | protected $response;
42 |
43 | /**
44 | * @var \Luthier\Routing\Route
45 | */
46 | protected $route;
47 |
48 | /**
49 | * Controller init
50 | *
51 | * @param ContainerInterface $container
52 | * @param Request $request
53 | * @param Response $response
54 | * @param Route $route
55 | *
56 | * @internal
57 | *
58 | * @return self
59 | */
60 | public function init(ContainerInterface $container, Request $request, Response $response, Route $route)
61 | {
62 | $this->container = $container;
63 | $this->request = $request;
64 | $this->response = $response;
65 | $this->route = $route;
66 | return $this;
67 | }
68 |
69 | /**
70 | * __get() magic method
71 | *
72 | * @param string $property
73 | *
74 | * @throws \Exception
75 | *
76 | * @return mixed
77 | */
78 | public function __get($property)
79 | {
80 | if ($this->container->has($property)) {
81 | return $this->container->get($property);
82 | } else {
83 | throw new \Exception("Trying to get undefined property $property");
84 | }
85 | }
86 |
87 | /**
88 | * Generates a route URL
89 | *
90 | * @param string $name Route name
91 | * @param array $params Route parameters
92 | *
93 | * @return string
94 | */
95 | public function route(string $name, array $params = [])
96 | {
97 | return $this->container->get('router')->getRouteByName($name, $params);
98 | }
99 | }
--------------------------------------------------------------------------------
/src/Database/Connection.php:
--------------------------------------------------------------------------------
1 |
20 | */
21 | class Connection extends \PDO
22 | {
23 |
24 | /**
25 | * @var ContainerInterface
26 | */
27 | private $container;
28 |
29 | /**
30 | * @param ContainerInterface $container
31 | */
32 | public function __construct(ContainerInterface $container)
33 | {
34 | $driver = $container->get('DB_TYPE');
35 | $host = $container->get('DB_HOST');
36 | $username = $container->get('DB_USER');
37 | $password = $container->get('DB_PASS');
38 | $database = $container->get('DB_NAME');
39 |
40 | $this->container = $container;
41 |
42 | parent::__construct("$driver:host=$host;dbname=$database", $username, $password, [
43 | \PDO::ATTR_ERRMODE => \PDO::ERRMODE_EXCEPTION
44 | ]);
45 | }
46 |
47 | /**
48 | * Loads a (namespaced) model
49 | *
50 | * @param string $model
51 | *
52 | * @return Model
53 | */
54 | public function model(string $model)
55 | {
56 | $namespace = $this->container->get('DB_MDNS');
57 |
58 | if (! empty($namespace)) {
59 | $model = $namespace . '\\' . $model;
60 | }
61 |
62 | return new $model($this->container, $this);
63 | }
64 | }
--------------------------------------------------------------------------------
/src/Database/Model.php:
--------------------------------------------------------------------------------
1 |
22 | */
23 | class Model
24 | {
25 | /**
26 | * @var Connection
27 | */
28 | protected $db;
29 |
30 | /**
31 | * @var ContainerInterface
32 | */
33 | protected $container;
34 |
35 | /**
36 | * @param ContainerInterface $container
37 | * @param Connection $conection
38 | */
39 | public function __construct(ContainerInterface $container, Connection $connection)
40 | {
41 | $this->db = new FluentPDO($connection);
42 |
43 | if ($container->get('APP_ENV') == 'development') {
44 | $this->db->debug = function ($builder) use ($container) {
45 | $query = $builder->getQuery(false);
46 |
47 | foreach ($builder->getParameters() as $value) {
48 | $fullQuery = preg_replace('/\?/', $value, $query, 1);
49 | }
50 |
51 | $container->get('logger')->debug($fullQuery, [
52 | 'DATABASE'
53 | ]);
54 | };
55 | }
56 |
57 | $this->container = $container;
58 | }
59 | }
--------------------------------------------------------------------------------
/src/Events/PreControllerEvent.php:
--------------------------------------------------------------------------------
1 |
21 | */
22 | class PreControllerEvent extends Event
23 | {
24 |
25 | /**
26 | * @var ContainerInterface
27 | */
28 | private $container;
29 |
30 | /**
31 | * @var array
32 | */
33 | private $middlewareStack;
34 |
35 | /**
36 | * @var callable
37 | */
38 | private $callback;
39 |
40 | /**
41 | * @var array
42 | */
43 | private $arguments;
44 |
45 | /**
46 | * @param ContainerInterface $container
47 | * @param array $middlewareStack
48 | * @param callable $callback
49 | * @param array $arguments
50 | */
51 | public function __construct(ContainerInterface $container, array &$middlewareStack, callable &$callback, array &$arguments)
52 | {
53 | $this->container = $container;
54 | $this->middlewareStack = &$middlewareStack;
55 | $this->callback = &$callback;
56 | $this->arguments = &$arguments;
57 | }
58 |
59 | /**
60 | * @return \Luthier\Http\Request
61 | */
62 | public function getRequest()
63 | {
64 | return $this->container->get('request');
65 | }
66 |
67 | /**
68 | * @return \Luthier\Http\Response
69 | */
70 | public function getResponse()
71 | {
72 | return $this->container->get('response');
73 | }
74 |
75 | /**
76 | * @return ContainerInterface
77 | */
78 | public function getContainer()
79 | {
80 | return $this->container;
81 | }
82 |
83 | /**
84 | * @return array
85 | */
86 | public function getMiddlewareStack()
87 | {
88 | return $this->middlewareStack;
89 | }
90 |
91 | /**
92 | * @return callable
93 | */
94 | public function getCallback()
95 | {
96 | return $this->callback;
97 | }
98 |
99 | /**
100 | * @return array
101 | */
102 | public function getArguments()
103 | {
104 | return $this->arguments;
105 | }
106 |
107 | /**
108 | * @param array $middlewareStack
109 | */
110 | public function setMiddlewareStack(array $middlewareStack)
111 | {
112 | $this->middlewareStack = $middlewareStack;
113 | }
114 |
115 | /**
116 | * @param callable $callback
117 | */
118 | public function setCallback(callable $callback)
119 | {
120 | $this->callback = $callback;
121 | }
122 |
123 | /**
124 | * @param array $arguments
125 | */
126 | public function setArguments(array $arguments)
127 | {
128 | $this->arguments = $arguments;
129 | }
130 | }
--------------------------------------------------------------------------------
/src/Events/RequestEvent.php:
--------------------------------------------------------------------------------
1 |
22 | */
23 | class RequestEvent extends Event
24 | {
25 |
26 | /**
27 | * @var ContainerInterface
28 | */
29 | private $container;
30 |
31 | /**
32 | * @param ContainerInterface $container
33 | */
34 | public function __construct(ContainerInterface $container)
35 | {
36 | $this->container = $container;
37 | }
38 |
39 | /**
40 | * @return \Luthier\Http\RequestInterface;
41 | */
42 | public function getRequest()
43 | {
44 | return $this->container->get('request');
45 | }
46 |
47 | /**
48 | * @return \Luthier\Http\ResponseInterface;
49 | */
50 | public function getResponse()
51 | {
52 | return $this->container->get('response');
53 | }
54 |
55 | /**
56 | * @return ContainerInterface
57 | */
58 | public function getContainer()
59 | {
60 | return $this->container;
61 | }
62 | }
--------------------------------------------------------------------------------
/src/Events/ResponseEvent.php:
--------------------------------------------------------------------------------
1 |
21 | */
22 | class ResponseEvent extends Event
23 | {
24 |
25 | /**
26 | * @var ContainerInterface
27 | */
28 | private $container;
29 |
30 | /**
31 | * @param ContainerInterface $container
32 | */
33 | public function __construct(ContainerInterface $container)
34 | {
35 | $this->container = $container;
36 | }
37 |
38 | /**
39 | * @return \Luthier\Http\RequestInterface;
40 | */
41 | public function getRequest()
42 | {
43 | return $this->container->get('request');
44 | }
45 |
46 | /**
47 | * @return \Luthier\Http\ResponseInterface;
48 | */
49 | public function getResponse()
50 | {
51 | return $this->container->get('response');
52 | }
53 |
54 | /**
55 | * @return ContainerInterface
56 | */
57 | public function getContainer()
58 | {
59 | return $this->container;
60 | }
61 | }
--------------------------------------------------------------------------------
/src/Framework.php:
--------------------------------------------------------------------------------
1 |
25 | */
26 | class Framework
27 | {
28 | use UtilsTrait;
29 |
30 | const VERSION = '0.1.1';
31 |
32 | /**
33 | * @var \Symfony\Component\HttpFoundation\Request
34 | */
35 | protected $sfRequest;
36 |
37 | /**
38 | * @var Container
39 | */
40 | protected $container;
41 |
42 | /**
43 | * @var Container
44 | */
45 | protected static $staticContainer;
46 |
47 | /**
48 | * @var Configuration
49 | */
50 | protected $config;
51 |
52 | /**
53 | * @var string
54 | */
55 | protected $appPath;
56 |
57 | /**
58 | * @var \Whoops\Run
59 | */
60 | protected $whoops;
61 |
62 | /**
63 | * @param mixed $config Application configuration
64 | * @param Container $container Application container
65 | * @param SfRequest $request Symfony request
66 | * @param string $appPath Application base path (by default the current working directory)
67 | *
68 | * @throws \Exception
69 | */
70 | public function __construct($config = null, ?Container $container = null, ?SfRequest $request = null, ?string $appPath = null)
71 | {
72 | $this->container = $container ?? new Container();
73 | $this->sfRequest = $request ?? SfRequest::createFromGlobals();
74 | $this->appPath = $appPath ?? getcwd();
75 |
76 | self::$staticContainer = &$this->container;
77 |
78 | if (is_array($config) || $config === null) {
79 | $this->config = new Configuration($config);
80 | } else if ($config instanceof Configuration) {
81 | $this->config = $config;
82 | } else {
83 | throw new \Exception("You must provide an array of configuration values or an instance of the Luthier\Configuration class");
84 | }
85 |
86 | // Auto .env config file setting
87 | if ($this->config->getEnvFolder() === null && file_exists($this->appPath . '/.env')) {
88 | $this->config->setEnvFolder($this->appPath);
89 | }
90 |
91 | // Setting the APP_PATH property
92 | if (! $this->container->has('APP_PATH') || ($this->container->has('APP_PATH') && empty($this->container->get('APP_PATH')))) {
93 | $this->container->parameter('APP_PATH', $this->appPath);
94 | }
95 |
96 | // Setting the APP_URL property
97 | if (! $this->container->has('APP_URL') || ($this->container->has('APP_URL') && empty($this->container->get('APP_PATH')))) {
98 | $this->container->parameter('APP_URL', $this->config->getConfigValue('APP_URL', null));
99 | }
100 |
101 | // At this point, only a few services are required, the rest will be
102 | // loaded during the application startup
103 | foreach (['router','dispatcher','response','request'] as $name) {
104 | if (! $this->container->has($name)) {
105 | [$type,$class,$aliases] = Container::getDefaultContainer()[$name];
106 | $this->container->{$type}($name, $class, $aliases);
107 | }
108 | }
109 | }
110 |
111 | /**
112 | * __call() magic method
113 | *
114 | * @return mixed
115 | */
116 | public function __call($method, $args)
117 | {
118 | return call_user_func_array([$this->container->get('router'),$method], $args);
119 | }
120 |
121 | /**
122 | * __get() magic method
123 | *
124 | * @return mixed
125 | */
126 | public function __get($property)
127 | {
128 | if ($this->container->has($property)) {
129 | return $this->container->get($property);
130 | } else {
131 | throw new \Exception("Trying to get undefined property Luthier\Framework::$property");
132 | }
133 | }
134 |
135 | /**
136 | * Sets the application Symfony request
137 | *
138 | * @param \Symfony\Component\HttpFoundation\Request $request Symfony Request object
139 | *
140 | * @return self
141 | */
142 | public function setRequest(SfRequest $request)
143 | {
144 | $this->sfRequest = $request;
145 | return $this;
146 | }
147 |
148 | /**
149 | * Sets the application dependency container
150 | *
151 | * @param \Luthier\Container $container The dependency injection container
152 | *
153 | * @return mixed
154 | */
155 | public function setContainer(Container $container)
156 | {
157 | $this->container = $container;
158 | return $this;
159 | }
160 |
161 | public static function getContainer()
162 | {
163 | return self::$staticContainer;
164 | }
165 |
166 | /**
167 | * Sets the application configuration
168 | *
169 | * @param Configuration $config
170 | *
171 | * @return self
172 | */
173 | public function setConfig(Configuration $config)
174 | {
175 | $this->config = $config;
176 | return $this;
177 | }
178 |
179 | /**
180 | * Luthier Framework error handler
181 | *
182 | * @param int $type
183 | * @param string $message
184 | * @param string $file
185 | * @param string $line
186 | *
187 | * @internal
188 | *
189 | * @return int
190 | */
191 | public function errorHandler($level, $message, $file = null, $line = null)
192 | {
193 | $error = $message;
194 |
195 | if (! empty($file)) {
196 | $error .= ' at ' . $file . ' on line ' . $line;
197 | }
198 |
199 | $this->container->get('logger')->error($error);
200 |
201 | if ($this->whoops === null) {
202 | $this->errorResponse($this->sfRequest)->send();
203 | } else {
204 | $this->whoops->handleError($level, $message, $file, $line);
205 | }
206 |
207 | exit(1);
208 | }
209 |
210 | /**
211 | * Internal exception handler
212 | *
213 | * @param \Exception $exception
214 | *
215 | * @internal
216 | * @return void
217 | */
218 | public function exceptionHandler($exception)
219 | {
220 | $error = 'Uncaught exception ' . get_class($exception) . ': ' . $exception->getMessage() . ' at ' . $exception->getFile() . ' on line ' . $exception->getLine() . PHP_EOL . 'Stack trace: ' . PHP_EOL . $exception->getTraceAsString();
221 | $this->container->get('logger')->error($error);
222 |
223 | if ($this->whoops === null) {
224 | $this->errorResponse($this->sfRequest)->send();
225 | } else {
226 | $this->whoops->handleException($exception);
227 | }
228 |
229 | exit(2);
230 | }
231 |
232 | /**
233 | * Shutdown handler
234 | *
235 | * @internal
236 | * @return void
237 | */
238 | public function shutdownHandler()
239 | {
240 | if ($error = error_get_last() and $error !== null && $error['type'] !== E_STRICT) {
241 | $this->errorHandler($error['type'], $error['message']);
242 | }
243 | exit(0);
244 | }
245 |
246 | /**
247 | * Configures the application
248 | *
249 | * @throws \Exception
250 | *
251 | * @internal
252 | * @return void
253 | */
254 | private function configure()
255 | {
256 | // Let's parse the provided configuration at this point
257 | $config = $this->config->parse();
258 |
259 | foreach ($config as $name => $value) {
260 | // Some configuration parameters (such APP_PATH) maybe are available
261 | // at this point, so we need to check if they aren't present in our
262 | // container
263 | if (! $this->container->has($name)) {
264 | $this->container->parameter($name, $value);
265 | }
266 | }
267 |
268 | // Container services/factories/parameters definition
269 | foreach (Container::getDefaultContainer() as $name => [$type,$class,$locatorAliases]) {
270 | if (! $this->container->has($name)) {
271 | $this->container->{$type}($name, $class, $locatorAliases);
272 | }
273 | }
274 |
275 | // Our container private services will be available as typehinted arguments
276 | // within the Router/RequestHandler
277 | $this->container->parameter('@PRIVATE_SERVICES', $this->container->getPrivateAliases());
278 |
279 | // Adding the (core) Request event subscriber
280 | $this->dispatcher->addSubscriber(new Http\EventSubscriber\RequestSubscriber());
281 |
282 | // The translation service is a special case: we use the Symfony translator component internally,
283 | if (! $this->container->has('translator')) {
284 | $this->container->service('translator', new \Symfony\Component\Translation\Translator($config['APP_LANG']), []);
285 | }
286 |
287 | if (! ($this->container->get('translator') instanceof \Symfony\Component\Translation\Translator)) {
288 | throw new \Exception("The translation service must be an instance of " . \Symfony\Component\Translation\Translator::class . "class");
289 | }
290 |
291 | // PHP runtime configuration
292 | switch ($config['APP_ENV']) {
293 | case 'development':
294 | error_reporting(- 1);
295 | ini_set('display_errors', 1);
296 | break;
297 | case 'production':
298 | error_reporting(0);
299 | ini_set('display_errors', 0);
300 | break;
301 | default:
302 | throw new \Exception('The application environment is not configured correctly');
303 | }
304 |
305 | // Error/exception reporting configuration
306 | if (! defined('LUTHIER_TEST_MODE') && $config['APP_ENV'] == 'development') {
307 |
308 | $this->whoops = new \Whoops\Run();
309 |
310 | $handler = ! $this->isCli()
311 | ? new \Whoops\Handler\PrettyPageHandler()
312 | : new \Whoops\Handler\PlainTextHandler();
313 |
314 | $this->whoops->pushHandler($handler);
315 | }
316 |
317 | set_error_handler([$this,'errorHandler']);
318 | set_exception_handler([$this,'exceptionHandler']);
319 | register_shutdown_function([$this,'shutdownHandler']);
320 |
321 | // Some useful helpers:
322 | require __DIR__ . '/Helpers.php';
323 | }
324 |
325 | /**
326 | * Runs the application
327 | *
328 | * @return void
329 | */
330 | public function run()
331 | {
332 | // Lets configure our application...
333 | $this->configure();
334 |
335 | $this->logger->debug('Luthier Framework v' . self::VERSION . ' (PHP ' . phpversion() . ') APP_PATH="' . $this->container->get('APP_PATH') . '"', ['CORE']);
336 |
337 | if (! $this->isCli()) {
338 | $this->runHttp();
339 | } else {
340 | $this->runCli();
341 | }
342 | }
343 |
344 | /**
345 | * Runs the application in HTTP mode
346 | *
347 | * @return void
348 | */
349 | private function runHttp()
350 | {
351 | $requestHandler = $this->container->get('request_handler');
352 |
353 | if (! empty($this->container->get('APP_CACHE'))) {
354 | $cacheFolder = $this->container->get('APP_PATH') . '/' . $this->container->get('APP_CACHE');
355 |
356 | if (! file_exists($cacheFolder)) {
357 | mkdir($cacheFolder . '/http', null, true);
358 | }
359 |
360 | $this->logger->debug('HttpKernel cache folder set to "' . $cacheFolder . '/http"', ['CORE']);
361 | $requestHandler = new HttpKernel\HttpCache\HttpCache($requestHandler, new HttpKernel\HttpCache\Store($cacheFolder . '/http'));
362 | }
363 |
364 | $requestHandler->handle($this->sfRequest)->send();
365 | }
366 |
367 | /**
368 | * Runs the application in CLI mode
369 | *
370 | * @return void
371 | */
372 | private function runCli()
373 | {
374 | $commandLoader = new FactoryCommandLoader($this->container->get('router')->getCommands());
375 | $application = new Application();
376 | $application->setCommandLoader($commandLoader);
377 | $application->run();
378 | }
379 | }
380 |
381 |
--------------------------------------------------------------------------------
/src/Helpers.php:
--------------------------------------------------------------------------------
1 | get('request')->getRequest()->attributes->get('_orig_route');
25 | }
26 |
27 | return \Luthier\Framework::getContainer()->get('router')->getRouteByName($name, $args, $absoluteUrl);
28 | }
29 |
30 | }
31 |
32 | if (!function_exists('url')) {
33 |
34 | /**
35 | * Generates an application URL
36 | *
37 | * @param string $url
38 | *
39 | * @see \Luthier\Http\RequestInterface::baseUrl()
40 | *
41 | * @return string
42 | */
43 | function url(string $url = '')
44 | {
45 | return \Luthier\Framework::getContainer()->get('')->baseUrl($url);
46 | }
47 |
48 | }
49 |
50 |
51 | if (!function_exists('redirect')) {
52 |
53 | /**
54 | * Sets the current response as a RedirectResponse to the given URL and
55 | * parameters
56 | *
57 | * @param string $url
58 | * @param int $status
59 | * @param array $headers
60 | *
61 | * @return void
62 | */
63 | function redirect(string $url = '', int $status = 302, array $headers = [])
64 | {
65 | \Luthier\Framework::getContainer()->get('response')->redirect($url, $status, $headers);
66 | }
67 |
68 | }
69 |
70 | if (!function_exists('route_redirect')) {
71 |
72 | /**
73 | * Sets the current response as a RedirectResponse to the URL of the given
74 | * route name
75 | *
76 | * @param string $name
77 | * @param array $params
78 | * @param number $status
79 | * @param array $headers
80 | *
81 | * @return void
82 | */
83 | function route_redirect($name, $params = [], $status = 302, $headers = [])
84 | {
85 | \Luthier\Framework::getContainer()->get('response')->routeRedirect($name, $params, $status, $headers);
86 | }
87 |
88 | }
89 |
90 | if (!function_exists('json')) {
91 |
92 | /**
93 | * Sets the current response as a JsonResponse with the given array of data
94 | *
95 | * @param array $data
96 | * @param number $status
97 | * @param array $headers
98 | *
99 | * @return void
100 | */
101 | function json_response(array $data, $status = 200, $headers = [])
102 | {
103 | \Luthier\Framework::getContainer()->get('response')->json($data, $status, $headers);
104 | }
105 |
106 | }
107 |
108 | if (!function_exists('xml_response')) {
109 |
110 | /**
111 | * Sets the current response as a XML response with the given array of data
112 | *
113 | * @param array $data
114 | * @param number $status
115 | * @param array $headers
116 | *
117 | * @return void
118 | */
119 | function xml_response(array $data, ?string $rootName = null, ?bool $translateSpaces = true, $status = 200, $headers = [])
120 | {
121 | \Luthier\Framework::getContainer()->get('response')->xml($data, $rootName, $translateSpaces, $status, $headers);
122 | }
123 |
124 | }
125 |
126 | if (!function_exists('view')) {
127 |
128 | /**
129 | * Renders a template
130 | *
131 | * @param string $template
132 | * @param array $vars
133 | * @param bool $return
134 | *
135 | * @return string|null
136 | */
137 | function view(string $template, array $vars = [], $return = false)
138 | {
139 | \Luthier\Framework::getContainer()->get('template')->render($template, $vars, $return);
140 | }
141 |
142 | }
143 |
144 | if (!function_exists('csrf_field')) {
145 |
146 | /**
147 | * Renders a hidden HTML input tag with the CSRF field
148 | *
149 | * @return string
150 | */
151 | function csrf_field()
152 | {
153 |
154 | }
155 |
156 | }
157 |
--------------------------------------------------------------------------------
/src/Http/EventSubscriber/RequestSubscriber.php:
--------------------------------------------------------------------------------
1 |
21 | */
22 | class RequestSubscriber implements EventSubscriberInterface
23 | {
24 | /**
25 | * @param Events\RequestEvent $event
26 | */
27 | public function onRequest(Events\RequestEvent $event)
28 | {
29 | $request = $event->getRequest();
30 | $this->setRequestMethodFromField($request);
31 | $this->parseJsonBody($request);
32 | }
33 |
34 | /**
35 | * If the request is a POST request and contains a field named "_method"
36 | * we will set the request method to the "_method" field value, allowing
37 | * us to use other methods than GET and POST via traditional html forms
38 | *
39 | * @param \Luthier\Http\Request $request
40 | * @internal
41 | */
42 | private function setRequestMethodFromField($request)
43 | {
44 | if (strtolower($request->getMethod()) == "post" && !$request->isAjax()) {
45 | $method = $request->post("_method");
46 | if (!empty($method)) {
47 | $request->setMethod(strtoupper($method));
48 | }
49 | }
50 | }
51 |
52 | /**
53 | * JSON body parsing. Useful when we recieve a request with a JSON
54 | * payload (Content-Type: application/json).
55 | *
56 | * @param \Luthier\Http\Request $request
57 | * @internal
58 | */
59 | private function parseJsonBody($request)
60 | {
61 | if ($request->getContentType() === 'json') {
62 | $jsonBody = json_decode($request->getContent(), true);
63 | $request->getRequest()->request->add($jsonBody);
64 | }
65 | }
66 |
67 | public static function getSubscribedEvents()
68 | {
69 | return [
70 | 'request' => ['onRequest', 0],
71 | ];
72 | }
73 | }
--------------------------------------------------------------------------------
/src/Http/Exception/InvalidCsrfTokenException.php:
--------------------------------------------------------------------------------
1 |
19 | */
20 | class InvalidCsrfTokenException extends \Exception
21 | {
22 | }
--------------------------------------------------------------------------------
/src/Http/Middleware/AjaxMiddleware.php:
--------------------------------------------------------------------------------
1 |
20 | */
21 | class AjaxMiddleware implements MiddlewareInterface
22 | {
23 |
24 | public function run($request, $response, $next)
25 | {
26 | if (! $request->isAjax()) {
27 | throw new NotFoundHttpException('This route is only available under AJAX requests');
28 | }
29 |
30 | $next($request, $response);
31 | }
32 | }
--------------------------------------------------------------------------------
/src/Http/Middleware/CsrfMiddleware.php:
--------------------------------------------------------------------------------
1 |
22 | */
23 | class CsrfMiddleware implements MiddlewareInterface
24 | {
25 | use UtilsTrait;
26 |
27 | /**
28 | * @var ContainerInterface
29 | */
30 | protected $container;
31 |
32 | /**
33 | * @param ContainerInterface $container
34 | */
35 | public function __construct(ContainerInterface $container)
36 | {
37 | $this->container = $container;
38 | }
39 |
40 | /**
41 | * {@inheritdoc}
42 | *
43 | * @see \Luthier\Http\Middleware\MiddlewareInterface::run()
44 | */
45 | public function run($request, $response, $next)
46 | {
47 | $csrf = $this->container->get('security');
48 |
49 | if ($csrf->getCsrfTokenName() !== null) {
50 | try
51 | {
52 | $csrf->verifyCsrfToken();
53 | return $next($request, $response);
54 | }
55 | catch(InvalidCsrfTokenException $e)
56 | {
57 | return $this->errorResponse($request->getRequest() ,400, null, 'Your request does not contain a valid CSRF token');
58 | }
59 | } else {
60 | $next($request, $response);
61 | }
62 | }
63 | }
--------------------------------------------------------------------------------
/src/Http/Middleware/MiddlewareInterface.php:
--------------------------------------------------------------------------------
1 |
18 | */
19 | interface MiddlewareInterface
20 | {
21 | /**
22 | * Runs the middleware
23 | *
24 | * @param \Luthier\Http\RequestInterface $request
25 | * @param \Luthier\Http\ResponseInterface $response
26 | * @param \Closure $next
27 | */
28 | public function run($request, $response, $next);
29 | }
30 |
--------------------------------------------------------------------------------
/src/Http/Middleware/ValidationMiddleware.php:
--------------------------------------------------------------------------------
1 |
22 | */
23 | class ValidationMiddleware implements MiddlewareInterface
24 | {
25 | use UtilsTrait;
26 |
27 | /**
28 | * @var ContainerInterface
29 | */
30 | protected $container;
31 |
32 | /**
33 | * @param ContainerInterface $container
34 | */
35 | public function __construct(ContainerInterface $container)
36 | {
37 | $this->container = $container;
38 | }
39 |
40 | /**
41 | * {@inheritdoc}
42 | *
43 | * @see \Luthier\Http\Middleware\MiddlewareInterface::run()
44 | */
45 | public function run($request, $response, $next)
46 | {
47 | try
48 | {
49 | $validationErrors = $request->sessionFlash->get('@VALIDATION_ERRORS');
50 |
51 | if (!$request->isAjax() && is_array($validationErrors) && !empty($validationErrors)) {
52 | $this->container->get('validator')->setValidationErrors($validationErrors);
53 | }
54 |
55 | $next($request, $response);
56 | }
57 | catch(ValidationConstraintException $e)
58 | {
59 | $validationErrors = $this->container->get('validator')->getValidationErrors();
60 | $validationMessage = 'Your request fails the validation constraints';
61 |
62 | if (! $request->isAjax()) {
63 |
64 | $referer = $request->header('referer');
65 | if (!empty($referer) && $referer != $request->getRequest()->getUri()){
66 | $request->sessionFlash->set(
67 | '@VALIDATION_ERRORS',
68 | $validationErrors
69 | );
70 | return $response->redirect($referer);
71 | }
72 |
73 | return $this->errorResponse($request->getRequest(), 400, null, $validationMessage);
74 | }
75 |
76 | return $response->json([
77 | 'error' => $validationMessage,
78 | 'fields' => $validationErrors,
79 | ],400);
80 |
81 | }
82 | }
83 | }
--------------------------------------------------------------------------------
/src/Http/Request.php:
--------------------------------------------------------------------------------
1 |
25 | */
26 | class Request implements RequestInterface
27 | {
28 | use UtilsTrait;
29 |
30 | /**
31 | * Symfony request object
32 | *
33 | * @var \Symfony\Component\HttpFoundation\Request
34 | */
35 | protected $request;
36 |
37 | /**
38 | * @var \Psr\Container\ContainerInterface
39 | */
40 | protected $container;
41 |
42 | /**
43 | * @param ContainerInterface $container
44 | */
45 | public function __construct(ContainerInterface $container)
46 | {
47 | $this->container = $container;
48 | }
49 |
50 | /**
51 | * {@inheritDoc}
52 | *
53 | * @see \Luthier\Http\RequestInterface::getRequest()
54 | */
55 | public function getRequest()
56 | {
57 | return $this->request;
58 | }
59 |
60 | /**
61 | * {@inheritDoc}
62 | *
63 | * @see \Luthier\Http\RequestInterface::setRequest()
64 | */
65 | public function setRequest(SfRequest $request = null)
66 | {
67 | $this->request = $request ?? SfRequest::createFromGlobals();
68 |
69 | return $this;
70 | }
71 |
72 | public function __get($property)
73 | {
74 | if (($property == 'session' || $property == 'sessionFlash') && !$this->request->hasSession()) {
75 | // Session start
76 | $sessionStorage = new NativeSessionStorage();
77 | $sessionStorage->setName($this->container->get('SESSION_NAME'));
78 | $this->request->setSession(new Session($sessionStorage));
79 | }
80 |
81 | if ($property == 'session') {
82 | return $this->request->getSession();
83 | } else if($property== 'sessionFlash') {
84 | return $this->request->getSession()->getFlashBag();
85 | } else {
86 | throw new \Exception("Try to get undefined Request::$property property");
87 | }
88 | }
89 |
90 | public function __call($method, $args)
91 | {
92 | $httpContainers = [
93 | 'attributes' => 'attributes',
94 | 'post' => 'request',
95 | 'request' => 'request',
96 | 'get' => 'query',
97 | 'server' => 'server',
98 | 'file' => 'files',
99 | 'header' => 'headers',
100 | 'cookie' => 'cookies',
101 | 'session' => null,
102 | 'sessionFlash' => null,
103 | ];
104 |
105 | if (in_array($method, array_keys($httpContainers))) {
106 | $name = $args[0] ?? NULL;
107 | $default = $args[1] ?? NULL;
108 |
109 | if ($method == 'session') {
110 | return $name !== NULL
111 | ? ($this->request->hasSession()
112 | ? $this->request->getSession()->get($name, $default)
113 | : null)
114 | : ($this->request->hasSession()
115 | ? $this->request->getSession()->all()
116 | : []);
117 | } else if($method == 'sessionFlash') {
118 | return $name !== NULL
119 | ? ($this->request->hasSession()
120 | ? $this->request->getSession()->getFlashBag()->get($name, $default ?? [])
121 | : null)
122 | : ($this->request->hasSession()
123 | ? $this->request->getSession()->getFlashBag()->all()
124 | : []);
125 | } else {
126 | return $name !== NULL
127 | ? $this->request->{$httpContainers[$method]}->get($name, $default)
128 | : $this->request->{$httpContainers[$method]}->all();
129 | }
130 | } else if(method_exists($this->request, $method)) {
131 | return call_user_func_array([$this->request, $method], $args);
132 | } else {
133 | throw new \BadMethodCallException("Call to undefined method Request::{$method}()");
134 | }
135 | }
136 |
137 | /**
138 | * Checks if the current request is an AJAX request (alias of Request::isXmlHttpRequest())
139 | *
140 | * @return bool
141 | */
142 | public function isAjax()
143 | {
144 | return $this->request->isXmlHttpRequest();
145 | }
146 |
147 | /**
148 | * {@inheritDoc}
149 | *
150 | * @see \Luthier\Http\RequestInterface::baseUrl()
151 | */
152 | public function baseUrl(string $url = '') : string
153 | {
154 | if ($this->container->get('APP_URL') !== null) {
155 | return $this->container->get('APP_URL') . '/' . trim($url, '/');
156 | }
157 |
158 | return trim($this->request->getScheme()
159 | . '://' . $this->request->getHost()
160 | . $this->request->getBasePath()
161 | . '/' . $url, '/');
162 | }
163 | }
--------------------------------------------------------------------------------
/src/Http/RequestHandler.php:
--------------------------------------------------------------------------------
1 |
34 | */
35 | class RequestHandler implements HttpKernelInterface
36 | {
37 | use UtilsTrait;
38 |
39 | /**
40 | * @var \Luthier\Container
41 | */
42 | protected $container;
43 |
44 | /**
45 | * @var \Luthier\Routing\RouteBuilder
46 | */
47 | protected $router;
48 |
49 | /**
50 | * @var \Symfony\Component\Routing\Matcher\UrlMatcher
51 | */
52 | protected $matcher;
53 |
54 | /**
55 | * @var \Symfony\Component\HttpKernel\Controller\ControllerResolver
56 | */
57 | protected $controllerResolver;
58 |
59 | /**
60 | * @var \Symfony\Component\HttpKernel\Controller\ArgumentResolver
61 | */
62 | protected $argumentResolver;
63 |
64 | /**
65 | * Luthier Request object
66 | *
67 | * @var \Luthier\Http\Request
68 | */
69 | protected $request;
70 |
71 | /**
72 | * Luthier Response object
73 | *
74 | * @var \Luthier\Http\Response
75 | */
76 | protected $response;
77 |
78 | /**
79 | * @var \Symfony\Component\EventDispatcher\EventDispatcher
80 | */
81 | protected $dispatcher;
82 |
83 | /**
84 | * @var \Monolog\Logger
85 | */
86 | protected $logger;
87 |
88 | /**
89 | * @param ContainerInterface $container Dependecy container
90 | */
91 | public function __construct(ContainerInterface $container)
92 | {
93 | $this->container = $container;
94 | $this->router = $container->get('router');
95 | $this->logger = $container->get('logger');
96 | $this->dispatcher = $container->get('dispatcher');
97 | $this->request = $container->get('request');
98 | $this->response = $container->get('response');
99 | $this->matcher = new UrlMatcher($this->router->getRoutes(), $this->router->getRequestContext());
100 | $this->argumentResolver = new ArgumentResolver();
101 | $this->controllerResolver = new ControllerResolver();
102 | }
103 |
104 | /**
105 | * @param SfRequest $request
106 | * @param array $match
107 | * @param Route $route
108 | *
109 | * @throws \Exception
110 | *
111 | * @return array ([$callback, $arguments])
112 | */
113 | private function resolveController(SfRequest &$request, array $match, Route $route)
114 | {
115 | // Both the (Luthier) Request and Response objects will be available
116 | // as typehinted parameters of the route callbacks/methods, just in case that
117 | // the developer prefers using it instead of the $this->request/$this->response
118 | // properties
119 | $match['request'] = $this->request;
120 | $match['response'] = $this->response;
121 |
122 | foreach ($this->container->get('@PRIVATE_SERVICES') as [
123 | $name,
124 | $class
125 | ]) {
126 | if (class_exists($class)) {
127 | $match[$name] = new $class($this->container);
128 | }
129 | }
130 |
131 | $request->attributes->add($match);
132 |
133 | $callback = $this->controllerResolver->getController($request);
134 |
135 | if ($callback instanceof \Closure) {
136 | // Is the route action a closure? Let's bind it to a new instance of
137 | // Luthier\Controller
138 | $callback = \Closure::bind($callback, (new Controller())->init($this->container, $this->request, $this->response, $route), Controller::class);
139 | } else if (isset($callback[0]) && $callback[0] instanceof Controller) {
140 | // Is the route action an instance of Luthier\Controller? then set the
141 | // container, request, response and current route
142 | $callback[0]->init($this->container, $this->request, $this->response, $route);
143 | } else {
144 | if (! isset($callback[0]) && ! is_object($callback[0])) {
145 | // Not a closure or an object? sorry, we will not handle that
146 | throw new \Exception("The route does not contain a valid callback");
147 | }
148 | }
149 |
150 | $arguments = $this->argumentResolver->getArguments($request, $callback);
151 |
152 | return [
153 | $callback,
154 | $arguments
155 | ];
156 | }
157 |
158 | /**
159 | * @param string $url
160 | * @param array $arguments
161 | * @param Route $route
162 | *
163 | * @return void
164 | */
165 | private function parseArguments(string $url, array &$arguments, Route $route)
166 | {
167 | $offset = 0;
168 |
169 | foreach ($arguments as $i => $arg) {
170 | if ($arg === null) {
171 | unset($arguments[$i]);
172 | }
173 | }
174 |
175 | foreach (explode('/', trim($url, '/')) as $i => $urlSegment) {
176 | $routeSegment = explode('/', $route->getFullPath())[$i];
177 | if (substr($routeSegment, 0, 1) == '{' && substr($routeSegment, - 1) == '}') {
178 | $route->params[$offset]->value = $urlSegment;
179 | $offset ++;
180 | }
181 | }
182 | }
183 |
184 | /**
185 | * @throws NotFoundHttpException
186 | *
187 | * @return \Symfony\Component\HttpFoundation\Response
188 | */
189 | private function welcomeScreen()
190 | {
191 | if ($this->container->get('APP_ENV') == 'production') {
192 | throw new NotFoundHttpException('Your application does not contain any route');
193 | }
194 |
195 | ob_start();
196 | require __DIR__ . '/../Resources/Views/About.php';
197 | $responseBody = ob_get_clean();
198 |
199 | return new SfResponse($responseBody);
200 | }
201 |
202 | /**
203 | * @param SfRequest $request
204 | * @param \Exception $exception
205 | *
206 | * @return void
207 | */
208 | private function handle404(SfRequest $request, \Exception $exception)
209 | {
210 | $env = $this->container->get('APP_ENV');
211 |
212 | $this->logger->warning('HTTP 404: Not found (' . $request->getMethod() . ' ' . $request->getUri() . ')', [
213 | "REQUEST_HANDLER"
214 | ]);
215 |
216 | if ($env == 'development') {
217 | throw $exception;
218 | }
219 |
220 | $httpNotFoundCallback = $this->router->getHttpNotFoundCallback();
221 |
222 | Response::getRealResponse($httpNotFoundCallback !== null ? call_user_func_array($httpNotFoundCallback, [
223 | $this->request,
224 | $this->response
225 | ]) : $this->errorResponse($request, 404, 'Not found', 'The requested resource is not available or has been moved to another location'), $this->response);
226 | }
227 |
228 | /**
229 | * @param SfRequest $request
230 | * @param Route $route
231 | * @param \Exception $exception
232 | *
233 | * @return void
234 | */
235 | private function handle405(SfRequest $request, ?Route $route, \Exception $exception)
236 | {
237 | $this->logger->warning('HTTP 405: Method not allowed (' . $request->getMethod() . ' ' . $request->getUri() . ')', [
238 | "REQUEST_HANDLER"
239 | ]);
240 |
241 | if ($this->container->get('APP_ENV') == 'development') {
242 | throw $exception;
243 | }
244 |
245 | $httpMethodNotAllowedCallback = $this->router->getHttpMethodNotAllowedCallback();
246 |
247 | Response::getRealResponse($httpMethodNotAllowedCallback !== null ? call_user_func_array($httpMethodNotAllowedCallback, [
248 | $this->request,
249 | $this->response,
250 | $route
251 | ]) : $this->errorResponse($request, 405, 'Not allowed', 'The request method is not allowed for this resource'), $this->response);
252 | }
253 |
254 | /**
255 | * @param mixed $finalResponse
256 | * @param Route $route
257 | * @param \Exception $exception
258 | *
259 | * @throws \Exception
260 | *
261 | * @return void
262 | */
263 | private function handleException(?Route $route, \Exception $exception)
264 | {
265 | $errorHandler = $this->router->getErrorHandler();
266 |
267 | if ($errorHandler !== NULL) {
268 | Response::getRealResponse(call_user_func_array($errorHandler, [
269 | $exception,
270 | $this->request,
271 | $this->response,
272 | $route
273 | ]), $this->response);
274 |
275 | $this->logger->error(get_class($exception) . ':' . $exception->getMessage(), [
276 | "REQUEST_HANDLER"
277 | ]);
278 | } else {
279 | throw $exception;
280 | }
281 | }
282 |
283 | /**
284 | * {@inheritDoc}
285 | *
286 | * @see \Symfony\Component\HttpKernel\HttpKernelInterface::handle()
287 | */
288 | public function handle(SfRequest $request, $type = HttpKernelInterface::MASTER_REQUEST, $catch = true)
289 | {
290 | $this->request->setRequest($request);
291 | $this->response->setResponse();
292 | $this->logger->debug($request->getMethod() . ' ' . $request->getUri(), [
293 | 'REQUEST_HANDLER'
294 | ]);
295 |
296 | // Dispatch the 'request' event
297 | $this->dispatcher->dispatch('request', new Events\RequestEvent($this->container));
298 |
299 | $this->matcher->getContext()->fromRequest($request);
300 |
301 | try {
302 |
303 | if ($this->router->count() == 0) {
304 | return $this->welcomeScreen();
305 | }
306 |
307 | $match = $this->matcher->match($request->getPathInfo());
308 |
309 | /** @var \Luthier\Routing\Route */
310 | $route = $match['_orig_route'];
311 |
312 | $this->router->setCurrentRoute($route);
313 |
314 | $this->logger->debug('Matched route ' . (! empty($route->getName()) ? '"' . $route->getName() . '"' : '') . ' (' . (! is_string($route->getAction()) ? '[anonymous@closure]' : $route->getAction()) . ') ' . 'for path "' . $route->getFullPath() . '" by router ' . get_class($this->router), [
315 | 'REQUEST_HANDLER'
316 | ]);
317 |
318 | [
319 | $callback,
320 | $arguments
321 | ] = $this->resolveController($request, $match, $route);
322 |
323 | $this->parseArguments($request->getPathInfo(), $arguments, $route);
324 |
325 | $middlewareStack = $this->router->getMiddlewareStack($route);
326 |
327 | // Dispatch the 'pre_controller' event
328 | $this->dispatcher->dispatch('pre_controller', new Events\PreControllerEvent($this->container, $middlewareStack, $callback, $arguments));
329 |
330 | // Now with the route callback/arguments and the middleware stack, we can
331 | // start iterating it
332 | ResponseIterator::handle($this->router, $middlewareStack, $callback, $arguments, $this->request, $this->response);
333 |
334 | } catch (ResourceNotFoundException | NotFoundHttpException $exception) {
335 | $this->handle404($request, $exception);
336 | } catch (MethodNotAllowedException | MethodNotAllowedHttpException $exception) {
337 | $this->handle405($request, $route ?? null, $exception);
338 | } catch (\Exception $exception) {
339 | $this->handleException($route ?? null, $exception);
340 | }
341 |
342 | // Dispatch the 'response' event
343 | $this->dispatcher->dispatch('response', new Events\ResponseEvent($this->container));
344 |
345 | return $this->response->getResponse();
346 | }
347 | }
348 |
349 |
--------------------------------------------------------------------------------
/src/Http/RequestInterface.php:
--------------------------------------------------------------------------------
1 |
21 | */
22 | interface RequestInterface
23 | {
24 |
25 | /**
26 | * Gets the Symfony Request
27 | *
28 | * @return \Symfony\Component\HttpFoundation\Request
29 | */
30 | public function getRequest();
31 |
32 | /**
33 | * Sets the Symfony Request
34 | *
35 | * @param Request $request
36 | */
37 | public function setRequest(Request $request);
38 |
39 | /**
40 | * Gets the application base url
41 | *
42 | * @param string $url
43 | *
44 | * @return string
45 | */
46 | public function baseUrl(string $url = ''): string;
47 | }
--------------------------------------------------------------------------------
/src/Http/Response.php:
--------------------------------------------------------------------------------
1 |
29 | */
30 | class Response implements ResponseInterface
31 | {
32 |
33 | /**
34 | * @var \Symfony\Component\HttpFoundation\Response
35 | */
36 | protected $response;
37 |
38 | /**
39 | * @var RequestInterface
40 | */
41 | protected $request;
42 |
43 | /**
44 | *
45 | * @var \Psr\Container\ContainerInterface;
46 | */
47 | protected $container;
48 |
49 | /**
50 | * @var \Luthier\Routing\RouteBuilder;
51 | */
52 | protected $router;
53 |
54 | /**
55 | * @param ContainerInterface $container
56 | */
57 | public function __construct(ContainerInterface $container)
58 | {
59 | $this->container = $container;
60 | $this->request = $container->get('request');
61 | $this->router = $container->get('router');
62 | }
63 |
64 | /**
65 | * {@inheritDoc}
66 | *
67 | * @see \Luthier\Http\ResponseInterface::getResponse()
68 | */
69 | public function getResponse()
70 | {
71 | return $this->response;
72 | }
73 |
74 | /**
75 | * {@inheritDoc}
76 | *
77 | * @see \Luthier\Http\ResponseInterface::setResponse()
78 | */
79 | public function setResponse(SfResponse $response = null)
80 | {
81 | $this->response = $response ?? new SfResponse();
82 | return $this;
83 | }
84 |
85 | public function __call($method, $args)
86 | {
87 | if (method_exists($this->response, $method)) {
88 | return call_user_func_array([
89 | $this->response,
90 | $method
91 | ], $args);
92 | } else {
93 | throw new \BadMethodCallException("Call to undefined method Response::{$method}()");
94 | }
95 | }
96 |
97 | public function __get($property)
98 | {
99 | if($property == 'headers'){
100 | return $this->response->headers;
101 | }
102 |
103 | throw new \Exception("Try to get undefined Request::$property property");
104 | }
105 |
106 | /**
107 | * Updates the internal Symfony Response object of this class if the
108 | * provided response is an instance of a Symfony Response too
109 | *
110 | * @param mixed $responseResult Response of an intermediary request
111 | * @param self $masterResponse The master response to be compared
112 | *
113 | * @internal
114 | *
115 | * @return void
116 | */
117 | public static function getRealResponse($responseResult, self $masterResponse)
118 | {
119 | if ($responseResult instanceof SfResponse) {
120 | $masterResponse->setResponse($responseResult);
121 | }
122 | }
123 |
124 | /**
125 | * {@inheritDoc}
126 | *
127 | * @see \Luthier\Http\ResponseInterface::json()
128 | */
129 | public function json($data, int $status = 200, array $headers = [])
130 | {
131 | $headers = array_merge($headers, $this->response->headers->all());
132 | $this->response = new JsonResponse(
133 | is_array($data)
134 | ? json_encode($data)
135 | : $data,
136 | $status,
137 | $headers,
138 | ! is_string($data)
139 | );
140 |
141 | return $this;
142 | }
143 |
144 | /**
145 | * Sets the response to a XML response with the given array of data
146 | *
147 | * @param array $data Array of data to be pased to XML
148 | * @param string $rootName Document root name
149 | * @param bool $translateSpaces Translate spaces to underscores?
150 | * @param int $status HTTP Stat
151 | * @param array $headers
152 | *
153 | * @return self
154 | */
155 | public function xml(array $data, ?string $rootName = null, ?bool $translateSpaces = true, int $status = 200, array $headers = [])
156 | {
157 | $this->response->setContent(ArrayToXml::convert($data, $rootName ?? '', $translateSpaces ?? true));
158 | $this->response->setStatusCode($status);
159 | $this->response->headers->add($headers);
160 | $this->response->headers->add(['Content-Type' => 'application/xml']);
161 |
162 | return $this;
163 | }
164 |
165 | /**
166 | * {@inheritDoc}
167 | *
168 | * @see \Luthier\Http\ResponseInterface::write()
169 | */
170 | public function write(string $content)
171 | {
172 | $this->response->setContent($this->response->getContent() . $content);
173 |
174 | return $this;
175 | }
176 |
177 | /**
178 | * {@inheritDoc}
179 | *
180 | * @see \Luthier\Http\ResponseInterface::redirect()
181 | */
182 | public function redirect(string $url, int $status = 302, array $headers = [])
183 | {
184 | if (substr($url, 0, 7) !== 'http://' && substr($url, 0, 8) !== 'https://') {
185 | $url = $this->request->baseUrl() . '/' . trim($url, '/');
186 | }
187 | $headers = array_merge($headers, $this->response->headers->all());
188 | $this->response = new RedirectResponse($url, $status, $headers);
189 |
190 | return $this;
191 | }
192 |
193 | /**
194 | * {@inheritDoc}
195 | *
196 | * @see \Luthier\Http\ResponseInterface::routeRedirect()
197 | */
198 | public function routeRedirect(string $route, array $params = [], int $status = 302, array $headers = [])
199 | {
200 | return $this->redirect($this->router->getRouteByName($route, $params), $status, $headers);
201 | }
202 |
203 | /**
204 | * Sets the response to a streamed response
205 | *
206 | * @param callable $callback Callback that produces the response
207 | * @param int $status HTTP status code
208 | * @param array $headers Additional HTTP headers
209 | *
210 | * @return self
211 | */
212 | public function stream(callable $callback, int $status = 200, array $headers = [])
213 | {
214 | $headers = array_merge($headers, $this->response->headers->all());
215 | $this->response = new StreamedResponse($callback, $status, $headers);
216 | return $this;
217 | }
218 |
219 | /**
220 | * Sets the response to a file stream response
221 | *
222 | * @param mixed $file File that will be streamed
223 | * @param int $status HTTP status code
224 | * @param array $headers Additional HTTP headers
225 | * @param bool $public Set the file as public
226 | * @param string $contentDisposition File content disposition
227 | * @param bool $autoEtag Add E-Tag automatically
228 | * @param bool $autoLastModified Set the Last Modified property automatically
229 | *
230 | * @return self
231 | */
232 | public function file($file, int $status = 200, array $headers = [], bool $public = true, string $contentDisposition = null, bool $autoEtag = false, bool $autoLastModified = true)
233 | {
234 | $headers = array_merge($headers, $this->response->headers->all());
235 | $this->response = new BinaryFileResponse($file, $status, $headers, $public, $contentDisposition, $autoEtag, $autoLastModified);
236 | return $this;
237 | }
238 | }
--------------------------------------------------------------------------------
/src/Http/ResponseInterface.php:
--------------------------------------------------------------------------------
1 |
21 | */
22 | interface ResponseInterface
23 | {
24 |
25 | /**
26 | * Gets the Symfony Response
27 | */
28 | public function getResponse();
29 |
30 | /**
31 | * Sets the Symfony Response
32 | *
33 | * @param Response $response
34 | */
35 | public function setResponse(Response $request);
36 |
37 | /**
38 | * Writes a text to the response
39 | *
40 | * @param string $string
41 | */
42 | public function write(string $string);
43 |
44 | /**
45 | * Sets the current response as a RedirectResponse with the provided parameters
46 | *
47 | * @param string $url The url to be redirected
48 | * @param int $status HTTP status code
49 | * @param array $headers Additional HTTP headers
50 | */
51 | public function redirect(string $url, int $status = 302, array $headers = []);
52 |
53 | /**
54 | * Sets the current response as a RedirectResponse to a specific route URL
55 | *
56 | * @param string $route Route name
57 | * @param array $params Route parameters
58 | * @param int $status HTTP status code
59 | * @param array $headers Additional HTTP headers
60 | */
61 | public function routeRedirect(string $route, array $params = [], int $status = 302, array $headers = []);
62 |
63 | /**
64 | * Sets the response to a JSON response
65 | *
66 | * @param string|array $data Json data
67 | * @param int $status HTTP status code
68 | * @param array $headers Additional HTTP headers
69 | */
70 | public function json($data, int $status = 200, array $headers = []);
71 | }
--------------------------------------------------------------------------------
/src/Http/ResponseIterator.php:
--------------------------------------------------------------------------------
1 |
22 | */
23 | class ResponseIterator
24 | {
25 | /**
26 | * @var int
27 | */
28 | private static $index;
29 |
30 | /**
31 | * @var array
32 | */
33 | private static $stack;
34 |
35 | /**
36 | * @var array
37 | */
38 | private static $callback;
39 |
40 | /**
41 | * @var \Closure
42 | */
43 | private static $router;
44 |
45 | /**
46 | * @param string|callable $middleware
47 | *
48 | * @return \Closure
49 | */
50 | private static function getMiddleware($middleware)
51 | {
52 | return self::$router->getMiddleware($middleware);
53 | }
54 |
55 | /**
56 | * Iterates over the provided request stack
57 | *
58 | * @param array $stack Request stack
59 | * @param callable $callback Route callback
60 | * @param array $arguments Route arguments
61 | * @param Request $request Luthier request
62 | * @param Response $response Luthier response
63 | *
64 | * @return \Symfony\Component\HttpFoundation\Response|mixed
65 | */
66 | public static function handle(RouteBuilder $router, array $stack, callable $callback, array $arguments, Request $request, Response $response)
67 | {
68 | self::$router = $router;
69 | self::$index = 0;
70 | self::$stack = $stack;
71 | self::$callback = [$callback, $arguments];
72 |
73 | if (count($stack) > 0) {
74 | $middleware = self::getMiddleware(self::$stack[0]);
75 | Response::getRealResponse($middleware($request, $response, function($request, $response){
76 | return \Luthier\Http\ResponseIterator::next($request, $response);
77 | }), $response);
78 | } else {
79 | Response::getRealResponse($callback(...$arguments), $response);
80 | }
81 | }
82 |
83 | /**
84 | * Returns the current middleware in the queue, or null if no more middleware
85 | * left
86 | *
87 | * @return array callable|null
88 | */
89 | public static function iterate()
90 | {
91 | return isset(self::$stack[++self::$index])
92 | ? self::$stack[self::$index]
93 | : null;
94 | }
95 |
96 | /**
97 | * Returns a callable of the next middleware in the queue based in the current
98 | * iterator index
99 | *
100 | * @param Request $request
101 | * @param Response $response
102 | *
103 | * @return callable|null
104 | */
105 | public static function next(Request $request, Response $response)
106 | {
107 | if ($response->getResponse() instanceof RedirectResponse) {
108 | return;
109 | }
110 |
111 | $middleware = self::iterate();
112 |
113 | if ($middleware === NULL) {
114 | [$callback, $arguments] = self::$callback;
115 | return $callback(...$arguments);
116 | } else {
117 | $middleware = self::getMiddleware($middleware);
118 | return $middleware($request, $response, function($request,$response){
119 | return \Luthier\Http\ResponseIterator::next($request, $response);
120 | });
121 | }
122 | }
123 | }
--------------------------------------------------------------------------------
/src/Http/Security.php:
--------------------------------------------------------------------------------
1 |
23 | */
24 | class Security
25 | {
26 | /**
27 | * @var ContainerInterface
28 | */
29 | protected $container;
30 |
31 | /**
32 | * @var Response
33 | */
34 | protected $response;
35 |
36 | /**
37 | * @var Request
38 | */
39 | protected $request;
40 |
41 | /**
42 | * @var string
43 | */
44 | protected $csrfTokenName;
45 |
46 | /**
47 | * @var string
48 | */
49 | protected $csrfTokenHash;
50 |
51 | /**
52 | * @var int
53 | */
54 | protected $csrfTokenCookieLifetime;
55 |
56 | /**
57 | * @var string
58 | */
59 | protected $csrfTokenCookiePath;
60 |
61 | /**
62 | * @var string
63 | */
64 | protected $csrfTokenCookieDomain;
65 |
66 | /**
67 | * @param ContainerInterface $container
68 | */
69 | public function __construct(ContainerInterface $container)
70 | {
71 | $this->container = $container;
72 |
73 | $csrfTokenName = $container->get('CSRF_TOKEN_NAME');
74 |
75 | $this->response = $container->get('response');
76 | $this->request = $container->get('request');
77 |
78 | if ($csrfTokenName !== null) {
79 | $this->csrfTokenName = $csrfTokenName;
80 | $this->initializeCsrfProtection();
81 | }
82 | }
83 |
84 | /**
85 | * @internal
86 | */
87 | private function initializeCsrfProtection()
88 | {
89 | $this->csrfTokenCookieLifetime = (int) $this->container->get('CSRF_TOKEN_LIFETIME');
90 | $this->csrfTokenCookieDomain = $this->container->get('CSRF_TOKEN_COOKIE_DOMAIN');
91 | $this->csrfTokenCookiePath = $this->container->get('CSRF_TOKEN_COOKIE_PATH');
92 |
93 | $this->setCsrfHash();
94 | }
95 |
96 | /**
97 | * Gets the current CSRF token hash
98 | *
99 | * @return string
100 | */
101 | public function getCsrfTokenHash()
102 | {
103 | return $this->csrfTokenHash;
104 | }
105 |
106 | /**
107 | * Gets the current CSRF token name
108 | *
109 | * @return string
110 | */
111 | public function getCsrfTokenName()
112 | {
113 | return $this->csrfTokenName;
114 | }
115 |
116 | /**
117 | * Checks if the request contains a valid CSRF token
118 | *
119 | * @throws Exception\InvalidCsrfTokenException
120 | *
121 | * @return Response|null
122 | */
123 | public function verifyCsrfToken()
124 | {
125 | if ( strtoupper($this->request->getMethod()) === "GET" ) {
126 | $this->response->headers->setCookie(
127 | new Cookie(
128 | $this->csrfTokenName,
129 | $this->csrfTokenHash,
130 | time() + $this->csrfTokenCookieLifetime,
131 | $this->csrfTokenCookiePath,
132 | $this->csrfTokenCookieDomain,
133 | $this->request->isSecure()
134 | )
135 | );
136 |
137 | return;
138 | }
139 |
140 | $requestToken = $this->request->getRequest()->request->get($this->csrfTokenName);
141 |
142 | if (! ($requestToken !== null && hash_equals($requestToken, $this->csrfTokenHash))) {
143 | throw new Exception\InvalidCsrfTokenException("Invalid or missing CSRF token");
144 | }
145 | }
146 |
147 | /**
148 | * Generates cryptographically secure pseudo-random bytes
149 | *
150 | * @param int $length
151 | * @throws \Exception
152 | * @return string
153 | */
154 | public function getRandomBytes(int $length)
155 | {
156 | if (function_exists('random_bytes')) {
157 | return bin2hex(random_bytes($length));
158 | }
159 | if (function_exists('mcrypt_create_iv')) {
160 | return bin2hex(mcrypt_create_iv($length, MCRYPT_DEV_URANDOM));
161 | }
162 | if (function_exists('openssl_random_pseudo_bytes')) {
163 | return bin2hex(openssl_random_pseudo_bytes($length));
164 | }
165 |
166 | throw new \Exception("No random bytes generator functions available!");
167 | }
168 |
169 | private function setCsrfHash()
170 | {
171 | if ($this->csrfTokenHash !== null) {
172 | return;
173 | }
174 |
175 | $tokenCookie = $this->request->cookie($this->csrfTokenName, null);
176 |
177 | if (!empty($tokenCookie) && preg_match('#^[0-9a-f]{32}$#iS', $tokenCookie)) {
178 | $this->csrfTokenHash = $tokenCookie;
179 | } else {
180 | $length = $this->container->get('CSRF_TOKEN_LENGTH');
181 | $this->csrfTokenHash = $this->getRandomBytes($length);
182 | }
183 | }
184 | }
--------------------------------------------------------------------------------
/src/Logger.php:
--------------------------------------------------------------------------------
1 |
23 | */
24 | class Logger extends MonologLogger
25 | {
26 |
27 | /**
28 | * @param ContainerInterface $container
29 | */
30 | public function __construct(ContainerInterface $container)
31 | {
32 | if (! empty($container->get('APP_LOG'))) {
33 | $log = $container->get('APP_PATH') . '/' . $container->get('APP_LOG');
34 |
35 | if (! file_exists($log)) {
36 | mkdir($log, null, true);
37 | }
38 |
39 | $log .= '/' . date('Y-m-d') . '.log';
40 |
41 | if (! file_exists($log)) {
42 | file_put_contents($log, '');
43 | }
44 |
45 | $this->name = $container->get('APP_NAME');
46 |
47 | $handler = new StreamHandler(
48 | $log,
49 | $container->get('APP_ENV') == 'development'
50 | ? MonologLogger::DEBUG
51 | : MonologLogger::ERROR
52 | );
53 | $handler->setFormatter(new LineFormatter(null, null, true, true));
54 |
55 | $this->handlers = [$handler];
56 | $this->processors = [];
57 | } else {
58 | // Failsafe instance construct
59 | parent::__construct("default");
60 | }
61 | }
62 | }
--------------------------------------------------------------------------------
/src/Resources/Views/About.php:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | Luthier Framework
6 |
7 |
8 |
9 |
45 |
46 |
47 |
57 |
58 |
--------------------------------------------------------------------------------
/src/Resources/Views/Error.php:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | = $title ?? 'Ups!' ?>
6 |
7 |
29 |
30 |
31 |
32 |
= $title ?? 'Ups!'?>
33 |
= $message ?? 'Something went wrong' ?>
34 |
35 |
36 |
--------------------------------------------------------------------------------
/src/Routing/Command.php:
--------------------------------------------------------------------------------
1 |
26 | */
27 | class Command
28 | {
29 |
30 | /**
31 | * @var string
32 | */
33 | protected $name;
34 |
35 | /**
36 | * @var callable|string
37 | */
38 | protected $callback;
39 |
40 | /**
41 | * @var string
42 | */
43 | protected $description = '';
44 |
45 | /**
46 | * @var string
47 | */
48 | protected $help = '';
49 |
50 | /**
51 | * @var array
52 | */
53 | protected $params = [];
54 |
55 | /**
56 | * @param string $name Command name
57 | * @param callable $callback Command callback
58 | */
59 | public function __construct(string $name, callable $callback)
60 | {
61 | $this->name = $name;
62 | $this->callback = $callback;
63 | }
64 |
65 | /**
66 | * Sets the parameter description
67 | *
68 | * @param string $description Description
69 | *
70 | * @return self
71 | */
72 | public function description(string $description)
73 | {
74 | $this->description = $description;
75 | return $this;
76 | }
77 |
78 | /**
79 | * Sets the parameter help
80 | *
81 | * @param string $help
82 | *
83 | * @return self
84 | */
85 | public function help(string $help)
86 | {
87 | $this->help = $help;
88 | return $this;
89 | }
90 |
91 | /**
92 | * Adds a new command parameter
93 | *
94 | * @param string $name Parameter name
95 | * @param mixed $shortcuts Parameter shortcuts
96 | * @param string $description Parameter description
97 | * @param string $mode Parameter mode (none|required|optional|array)
98 | * @param mixed $default Set the parameter default value
99 | *
100 | * @throws \Exception
101 | *
102 | * @return self
103 | */
104 | public function param(string $name, $shortcuts = null, string $description = '', string $mode = 'required', $default = null)
105 | {
106 | $modes = [
107 | 'none' => InputOption::VALUE_NONE,
108 | 'required' => InputOption::VALUE_REQUIRED,
109 | 'optional' => InputOption::VALUE_OPTIONAL,
110 | 'array' => InputOption::VALUE_IS_ARRAY
111 | ];
112 |
113 | if (! in_array($mode, $modes)) {
114 | throw new \Exception("Unknown command parameter '$mode' mode");
115 | }
116 |
117 | $this->params[] = new InputOption($name, $shortcuts, $mode, $description, $default);
118 |
119 | return $this;
120 | }
121 |
122 | /**
123 | * Gets the command name
124 | *
125 | * @return string
126 | */
127 | public function getName()
128 | {
129 | return $this->name;
130 | }
131 |
132 | /**
133 | * Gets the command callback
134 | *
135 | * @return string|callable
136 | */
137 | public function getCallback()
138 | {
139 | return $this->callback;
140 | }
141 |
142 | /**
143 | * Gets the command description
144 | *
145 | * @return string
146 | */
147 | public function getDescription()
148 | {
149 | return $this->description;
150 | }
151 |
152 | /**
153 | * Gets the command help
154 | *
155 | * @return string
156 | */
157 | public function getHelp()
158 | {
159 | return $this->help;
160 | }
161 |
162 | /**
163 | * Gets the command parameters
164 | *
165 | * @return array
166 | */
167 | public function getParams()
168 | {
169 | return $this->params;
170 | }
171 |
172 | /**
173 | * Compiles the command to a Symfony Application command
174 | *
175 | * @return \Symfony\Component\Console\Command\Command
176 | */
177 | public function compile()
178 | {
179 | $_command = &$this;
180 |
181 | return new Class($_command) extends SfCommand {
182 |
183 | private $_command;
184 |
185 | public function __construct($_command)
186 | {
187 | $this->_command = $_command;
188 | parent::__construct($_command->getName());
189 | }
190 |
191 | public function configure()
192 | {
193 | $this->setName($this->_command->getName());
194 | $this->setDescription($this->_command->getDescription());
195 | $this->setHelp($this->_command->getHelp());
196 | if (! empty($this->_command->getParams())) {
197 | $this->setDefinition(new InputDefinition($this->_command->getParams()));
198 | }
199 | }
200 |
201 | public function execute(InputInterface $input, OutputInterface $output)
202 | {
203 | $callback = \Closure::bind($this->_command->getCallback(), $this, SfCommand::class);
204 | call_user_func_array($callback, [
205 | $input,
206 | $output
207 | ]);
208 | }
209 | };
210 | }
211 | }
--------------------------------------------------------------------------------
/src/Routing/Exception/RouteNotFoundException.php:
--------------------------------------------------------------------------------
1 |
18 | */
19 | class RouteNotFoundException extends \Exception
20 | {
21 | }
--------------------------------------------------------------------------------
/src/Routing/Route.php:
--------------------------------------------------------------------------------
1 |
20 | */
21 | class Route
22 | {
23 |
24 | /**
25 | * Relative path
26 | *
27 | * @var string
28 | */
29 | private $path;
30 |
31 | /**
32 | * Full (absolute) path
33 | *
34 | * @var string
35 | */
36 | private $fullPath;
37 |
38 | /**
39 | * @var string
40 | */
41 | private $name;
42 |
43 | /**
44 | * Accepted HTTP Verbs
45 | *
46 | * @var string[]
47 | *
48 | * @access private
49 | */
50 | private $methods = [];
51 |
52 | /**
53 | * @var string|callable
54 | */
55 | private $action;
56 |
57 | /**
58 | * @var array
59 | */
60 | private $middleware = [];
61 |
62 | /**
63 | * @var string
64 | */
65 | private $namespace = '';
66 |
67 | /**
68 | * @var string
69 | */
70 | private $prefix = '';
71 |
72 | /**
73 | * @var string
74 | */
75 | private $host = '';
76 |
77 | /**
78 | * @var string[]
79 | */
80 | private $schemes = [];
81 |
82 | /**
83 | * @var RouteParam[] Array of route parameters
84 | */
85 | public $params = [];
86 |
87 | /**
88 | * @var int
89 | */
90 | public $paramOffset;
91 |
92 | /**
93 | * @var bool
94 | */
95 | public $hasOptionalParams = false;
96 |
97 | /**
98 | * @param array|string $methods Route methods
99 | * @param array $route Route arguments
100 | *
101 | * @throws \Exception
102 | */
103 | public function __construct($methods, array $route)
104 | {
105 | if ($methods == 'any') {
106 | $methods = RouteBuilder::HTTP_VERBS;
107 | } elseif (is_string($methods)) {
108 | $methods = [
109 | strtoupper($methods)
110 | ];
111 | } else {
112 | array_shift($route);
113 | }
114 |
115 | foreach ($methods as $method) {
116 | $this->methods[] = strtoupper($method);
117 | }
118 |
119 | [
120 | $path,
121 | $action
122 | ] = $route;
123 |
124 | $this->path = trim($path, '/') == '' ? '/' : trim($path, '/');
125 |
126 | if (! is_callable($action) && count(explode('@', $action)) != 2) {
127 | throw new \Exception('Route action must be in controller@method syntax or be a valid callback');
128 | }
129 |
130 | $this->action = $action;
131 |
132 | $attributes = isset($route[2]) && is_array($route[2]) ? $route[2] : NULL;
133 |
134 | if (! empty(RouteBuilder::getContext('prefix'))) {
135 | $prefixes = RouteBuilder::getContext('prefix');
136 | foreach ($prefixes as $prefix) {
137 | $this->prefix .= trim($prefix, '/') != '' ? '/' . trim($prefix, '/') : '';
138 | }
139 | $this->prefix = trim($this->prefix, '/');
140 | }
141 |
142 | if (! empty(RouteBuilder::getContext('namespace'))) {
143 | $namespaces = RouteBuilder::getContext('namespace');
144 | foreach ($namespaces as $namespace) {
145 | $this->namespace .= trim($namespace, '/') != '' ? '/' . trim($namespace, '/') : '';
146 | }
147 | $this->namespace = trim($this->namespace, '/');
148 | }
149 |
150 | if (! empty(RouteBuilder::getContext('middleware')['route'])) {
151 | $middleware = RouteBuilder::getContext('middleware')['route'][0];
152 | foreach ($middleware as $_middleware) {
153 | if (! in_array($_middleware, $this->middleware)) {
154 | $this->middleware[] = $_middleware;
155 | }
156 | }
157 | }
158 |
159 | if (! empty(RouteBuilder::getContext('host'))) {
160 | $host = RouteBuilder::getContext('host')[0];
161 | $this->host = $host;
162 | }
163 |
164 | if (! empty(RouteBuilder::getContext('schemes'))) {
165 | $schemes = RouteBuilder::getContext('schemes');
166 | if (! empty($schemes)) {
167 | $this->schemes = $schemes[0];
168 | }
169 | }
170 |
171 | if ($attributes !== NULL) {
172 | if (isset($attributes['namespace'])) {
173 | $this->namespace = (! empty($this->namespace) ? '/' : '') . trim($attributes['namespace'], '/');
174 | }
175 | if (isset($attributes['prefix'])) {
176 | $this->prefix .= (! empty($this->prefix) ? '/' : '') . trim($attributes['prefix'], '/');
177 | }
178 | if (isset($attributes['middleware'])) {
179 | if (is_string($attributes['middleware'])) {
180 | $attributes['middleware'] = [
181 | $attributes['middleware']
182 | ];
183 | }
184 | $this->middleware = array_merge($this->middleware, array_unique($attributes['middleware']));
185 | }
186 | }
187 |
188 | $params = [];
189 | $this->fullPath = [];
190 |
191 | $_fullpath = trim($this->prefix, '/') != '' ? $this->prefix . '/' . $this->path : $this->path;
192 |
193 | $_fullpath = trim($_fullpath, '/') == '' ? '/' : trim($_fullpath, '/');
194 |
195 | foreach (explode('/', $_fullpath) as $i => $segment) {
196 | if (preg_match('/^\{(.*)\}$/', $segment)) {
197 | if ($this->paramOffset === null) {
198 | $this->paramOffset = $i;
199 | }
200 |
201 | $param = new RouteParam($segment);
202 |
203 | if (in_array($param->getName(), $params)) {
204 | throw new \Exception('Duplicate route parameter "' . $param->getName() . '" in route "' . $this->path . '"');
205 | }
206 |
207 | $params[] = $param->getName();
208 |
209 | if ($param->isOptional()) {
210 | $this->hasOptionalParams = true;
211 | } else {
212 | if ($this->hasOptionalParams) {
213 | throw new \Exception('Required "' . $param->getName() . '" route parameter is not allowed at this position in "' . $this->path . '" route');
214 | }
215 | }
216 |
217 | $this->params[] = $param;
218 | $this->fullPath[] = '{' . $param->getName() . ($param->isOptional() ? '?' : '') . '}';
219 | } else {
220 | $this->fullPath[] = $segment;
221 | }
222 | }
223 |
224 | $this->fullPath = implode('/', $this->fullPath);
225 | }
226 |
227 | /**
228 | * Compiles the route to a Symfony route
229 | *
230 | * @return array ([string $name, \Symfony\Component\Routing\Route $route])
231 | */
232 | public function compile()
233 | {
234 | $path = $this->fullPath;
235 |
236 | if (is_callable($this->action)) {
237 | $controller = $this->action;
238 | } else {
239 | $controller = (! empty($this->namespace) ? $this->namespace . '\\' : '') . implode('::', explode('@', $this->action));
240 | }
241 |
242 | $defaults = [
243 | '_controller' => $controller,
244 | '_orig_route' => $this
245 | ];
246 |
247 | $requirements = [];
248 |
249 | foreach ($this->params as $param) {
250 | $requirements[$param->getName()] = $param->getRegex();
251 | }
252 |
253 | $options = [];
254 | $host = $this->host;
255 | $schemes = $this->schemes;
256 | $methods = $this->methods;
257 |
258 | return [
259 | $this->name,
260 | new SfRoute($path, $defaults, $requirements, $options, $host, $schemes, $methods)
261 | ];
262 | }
263 |
264 | /**
265 | * Gets (or sets) a route parameter
266 | *
267 | * @param string $name Parameter name
268 | * @param mixed $value Parameter value
269 | *
270 | * @return mixed|void
271 | */
272 | public function param(string $name, $value = null)
273 | {
274 | foreach ($this->params as &$param) {
275 | if ($name == $param->getName()) {
276 | if ($value !== null) {
277 | $param->value = $value;
278 | }
279 | return $param->value;
280 | }
281 | }
282 | }
283 |
284 | /**
285 | * Sets the route name
286 | *
287 | * @param string $name Route name
288 | *
289 | * @return self
290 | */
291 | public function name(string $name)
292 | {
293 | $this->name = $name;
294 | return $this;
295 | }
296 |
297 | /**
298 | * Adds a middleware to route
299 | *
300 | * @param mixed $middleware Route middleware
301 | *
302 | * @return self
303 | */
304 | public function middleware($middleware)
305 | {
306 | if (is_array($middleware)) {
307 | $this->middleware = array_merge($this->middleware, $middleware);
308 | } else {
309 | $this->middleware[] = $middleware;
310 | }
311 | return $this;
312 | }
313 |
314 | /**
315 | * Sets the route host
316 | *
317 | * @param string $host Route host
318 | *
319 | * @return self
320 | */
321 | public function host(string $host)
322 | {
323 | $this->host = $host;
324 | return $this;
325 | }
326 |
327 | /**
328 | * Sets the route accepted HTTP schemes
329 | *
330 | * @param array $schemes
331 | *
332 | * @return self
333 | */
334 | public function schemes(array $schemes)
335 | {
336 | $this->schemes = $schemes;
337 | return $this;
338 | }
339 |
340 | /**
341 | * Adds the built-in AJAX middleware to the current route
342 | *
343 | * @return self
344 | */
345 | public function ajax()
346 | {
347 | $this->middleware[] = new \Luthier\Http\Middleware\AjaxMiddleware();
348 | return $this;
349 | }
350 |
351 | /**
352 | * Checks if the route has a specific parameter
353 | *
354 | * @param string $name Parameter name
355 | *
356 | * @return bool
357 | */
358 | public function hasParam(string $name)
359 | {
360 | foreach ($this->params as &$param) {
361 | if ($name == $param->getName()) {
362 | return true;
363 | }
364 | }
365 | return false;
366 | }
367 |
368 | /**
369 | * Gets the route name
370 | *
371 | * @return string
372 | *
373 | * @access public
374 | */
375 | public function getName()
376 | {
377 | return $this->name;
378 | }
379 |
380 | /**
381 | * Sets route name [alias of Route::name()]
382 | *
383 | * @param string $name Route name
384 | *
385 | * @return self
386 | */
387 | public function setName(string $name)
388 | {
389 | return $this->name($name);
390 | }
391 |
392 | /**
393 | * Gets route path
394 | *
395 | * @return string
396 | */
397 | public function getPath()
398 | {
399 | return $this->path;
400 | }
401 |
402 | /**
403 | * Get route full (absolute) path
404 | *
405 | * @return string
406 | */
407 | public function getFullPath()
408 | {
409 | return $this->fullPath;
410 | }
411 |
412 | /**
413 | * Gets route prefix
414 | *
415 | * @return string
416 | */
417 | public function getPrefix()
418 | {
419 | return $this->prefix;
420 | }
421 |
422 | /**
423 | * Gets route action
424 | *
425 | * @return string|callable
426 | *
427 | */
428 | public function getAction()
429 | {
430 | return $this->action;
431 | }
432 |
433 | /**
434 | * Gets route middleware
435 | *
436 | * @return array
437 | */
438 | public function getMiddleware()
439 | {
440 | return $this->middleware;
441 | }
442 |
443 | /**
444 | * Gets route namespace
445 | *
446 | * @return string
447 | */
448 | public function getNamespace()
449 | {
450 | return $this->namespace;
451 | }
452 |
453 | /**
454 | * Gets route accepted HTTP verbs
455 | *
456 | * @return array
457 | */
458 | public function getMethods()
459 | {
460 | return $this->methods;
461 | }
462 |
463 | /**
464 | * Gets route host
465 | *
466 | * @return string
467 | */
468 | public function getHost()
469 | {
470 | return $this->host;
471 | }
472 |
473 | /**
474 | * Gets route accepted HTTP schemes
475 | *
476 | * @return array
477 | */
478 | public function getSchemes()
479 | {
480 | return $this->schemes;
481 | }
482 |
483 | /**
484 | * Gets all route sticky parameters
485 | *
486 | * @return array
487 | */
488 | public function getStickyParams()
489 | {
490 | $sticky = [];
491 |
492 | foreach ($this->params as $param) {
493 | if ($param->isSticky()) {
494 | $sticky[] = $param->getName();
495 | }
496 | }
497 |
498 | return $sticky;
499 | }
500 |
501 | /**
502 | * Sets the route path
503 | *
504 | * @param string $path Route path
505 | *
506 | * @return self
507 | */
508 | public function setPath(string $path)
509 | {
510 | $this->path = $path;
511 | return $this;
512 | }
513 |
514 | /**
515 | * Sets the route action
516 | *
517 | * @param string|callable $action Route action
518 | *
519 | * @return self
520 | */
521 | public function setAction($action)
522 | {
523 | $this->action = $action;
524 | return $this;
525 | }
526 |
527 | /**
528 | * Sets the route host
529 | *
530 | * @param string $host
531 | *
532 | * @return self
533 | */
534 | public function setHost(string $host)
535 | {
536 | return $this->host($host);
537 | }
538 |
539 | /**
540 | * Sets the route accepted HTTP schemes (method chaining)
541 | *
542 | * @param array $schemes Accepted HTTP schemes
543 | *
544 | * @return self
545 | */
546 | public function setSchemes(array $schemes)
547 | {
548 | return $this->schemes($schemes);
549 | }
550 | }
--------------------------------------------------------------------------------
/src/Routing/RouteBuilder.php:
--------------------------------------------------------------------------------
1 |
46 | */
47 | class RouteBuilder implements RouteBuilderInterface
48 | {
49 |
50 | const HTTP_VERBS = ['GET','POST','PUT','PATCH','DELETE','HEAD','OPTIONS','TRACE'];
51 |
52 | /**
53 | * @var ContainerInterface
54 | */
55 | protected $container;
56 |
57 | /**
58 | * @var array $context
59 | */
60 | protected static $context = [
61 | 'middleware' => [
62 | 'route' => [],
63 | 'global' => [],
64 | 'alias' => []
65 | ],
66 | 'namespace' => [],
67 | 'prefix' => [],
68 | 'params' => [],
69 | 'host' => [],
70 | 'schemes' => []
71 | ];
72 |
73 | /**
74 | * @var Command[] $commands
75 | */
76 | protected $commands = [];
77 |
78 | /**
79 | * @var Route[] $routes
80 | */
81 | protected $routes = [];
82 |
83 | /**
84 | * @var array $names
85 | */
86 | protected $names = [];
87 |
88 | /**
89 | * @var RouteCollection $routeCollection
90 | */
91 | protected $routeCollection;
92 |
93 | /**
94 | * @var UrlGenerator $routeGenerator
95 | */
96 | protected $routeGenerator;
97 |
98 | /**
99 | * @var RequestContext $requestContext
100 | */
101 | protected $requestContext;
102 |
103 | /**
104 | * HTTP/1.1 404: Not Found error callback
105 | *
106 | * @var callable $httpNotFoundCallback
107 | */
108 | protected $httpNotFoundCallback;
109 |
110 | /**
111 | * HTTP/1.1 405: Method Not Allowed error callback
112 | *
113 | * @var callable $httpNotAllowedCallback
114 | */
115 | protected $httpMethodNotAllowedCallback;
116 |
117 | /**
118 | * Error Handler used by the Request Handler (HttpKernel)
119 | *
120 | * @var callable $errorCallback
121 | */
122 | protected $errorHandler;
123 |
124 | /**
125 | * @var \Luthier\Routing\Route
126 | */
127 | protected $currentRoute;
128 |
129 | /**
130 | * @var bool $isSandboxed
131 | */
132 | protected static $isSandboxed = false;
133 |
134 | public function __construct(ContainerInterface $container)
135 | {
136 | $this->container = $container;
137 | $this->routeCollection = new RouteCollection();
138 | $this->requestContext = new RequestContext();
139 |
140 | // Built-in middleware
141 | $this->middleware('ajax', AjaxMiddleware::class);
142 | $this->middleware('csrf', CsrfMiddleware::class);
143 | $this->middleware('validation', ValidationMiddleware::class);
144 | }
145 |
146 | public function __call($callback, array $attributes)
147 | {
148 | if ($callback == 'command') {
149 | [$name,$_callback] = $attributes;
150 |
151 | $command = new LuthierCommand($name, $_callback);
152 |
153 | if (isset($this->commands[$command->getName()])) {
154 | echo 'ERROR: Duplicated ' . $command->getName() . ' command!' . PHP_EOL;
155 | exit(- 1);
156 | }
157 |
158 | $this->commands[$command->getName()] = function () use ($command) {
159 | return $command->compile();
160 | };
161 |
162 | return $command;
163 | } else if (in_array(strtoupper($callback), self::HTTP_VERBS) || in_array($callback, ['match', 'any'])) {
164 |
165 | if ($callback == 'match') {
166 | $methods = $attributes[0];
167 | } else {
168 | $methods = $callback;
169 | }
170 |
171 | $route = new LuthierRoute($methods, $attributes);
172 | $this->routes[] = $route;
173 | return $route;
174 | } else {
175 | throw new \BadMethodCallException("Call to undefined method Luthier\RouteBuilder::{$callback}() ");
176 | }
177 | }
178 |
179 | /**
180 | * Creates a new route group
181 | *
182 | * Two syntax are accepted:
183 | *
184 | * // Only prefix and sub-routes
185 | * $app->group('prefix', function(){ ... });
186 | *
187 | * // Both prefix and shared attributes:
188 | * $app->group('prefix, [ 'attrs' ], function(){ ... });
189 | *
190 | * Inside the sub-routes definition callback, the router instance is binded to
191 | * the `$this` variable
192 | *
193 | * @param string $prefix Group URL prefix
194 | * @param mixed $attributes Group shared attributes
195 | * @param mixed $routes Group sub-routes definition callback
196 | *
197 | * @throws \Exception
198 | *
199 | * @return void
200 | */
201 | public function group(string $prefix, $attributes, $routes = null)
202 | {
203 | if ($routes === null && is_callable($attributes)) {
204 | $routes = $attributes;
205 | $attributes = [];
206 | }
207 |
208 | self::$context['prefix'][] = $prefix;
209 |
210 | if (isset($attributes['namespace'])) {
211 | self::$context['namespace'][] = $attributes['namespace'];
212 | }
213 |
214 | if (isset($attributes['schemes'])) {
215 | self::$context['schemes'][] = $attributes['schemes'];
216 | }
217 |
218 | if (isset($attributes['middleware'])) {
219 | if (! is_array($attributes['middleware']) && ! is_string($attributes['middleware'])) {
220 | throw new \Exception('Route group middleware must be an array o a string');
221 | }
222 |
223 | if (is_string($attributes['middleware'])) {
224 | $attributes['middleware'] = [$attributes['middleware']];
225 | }
226 |
227 | self::$context['middleware']['route'][] = $attributes['middleware'];
228 | }
229 |
230 | if (isset($attributes['host'])) {
231 | self::$context['host'] = $attributes['host'];
232 | }
233 |
234 | $routes = \Closure::bind($routes, $this, RouteBuilder::class);
235 | $routes();
236 |
237 | array_pop(self::$context['prefix']);
238 |
239 | if (isset($attributes['namespace'])) {
240 | array_pop(self::$context['namespace']);
241 | }
242 |
243 | if (isset($attributes['middleware'])) {
244 | array_pop(self::$context['middleware']['route']);
245 | }
246 |
247 | if (isset($attributes['schemes'])) {
248 | array_pop(self::$context['schemes']);
249 | }
250 |
251 | if (isset($attributes['host'])) {
252 | self::$context['host'] = NULL;
253 | }
254 | }
255 |
256 | /**
257 | * Defines (or runs) a global middleware
258 | *
259 | * The following syntax is accepted:
260 | *
261 | * // Defining a global middlware
262 | *
263 | * # Closure:
264 | * $app->middleware('alias', function(Request $request, Response $response, Closure $next){ ... });
265 | *
266 | * # Callable:
267 | * $app->middleware('alias', 'middleware_callable');
268 | *
269 | * // Runing a global middleware
270 | *
271 | * # From alias:
272 | * $app->middleware('alias');
273 | *
274 | * # From a closure:
275 | * $app->middleware(function(Request $request, Response $response, Closure $next){ ... });
276 | *
277 | * You can also pass an array of middleware closures/alias to be executed:
278 | *
279 | * $app->middleware(['foo','bar','baz']);
280 | *
281 | * @param string|callable|array $middleware Middleware to be defined or executed
282 | *
283 | * @throws \InvalidArgumentException
284 | *
285 | * @return void
286 | */
287 | public function middleware($middleware)
288 | {
289 | if (count(func_get_args()) == 2) {
290 | [$name,$middleware] = func_get_args();
291 |
292 | if (! is_string($name)) {
293 | throw new \InvalidArgumentException("The middleware alias must be a string");
294 | }
295 |
296 | if (! is_callable($middleware) && ! class_exists($middleware)) {
297 | throw new \InvalidArgumentException("Invalid middleware definition. Must be a valid callback." . (is_string($middleware) ? " (Does the '$middleware' class exists?)" : ''));
298 | }
299 |
300 | self::$context['middleware']['alias'][$name] = $middleware;
301 | } else {
302 | if (! is_array($middleware)) {
303 | $middleware = [$middleware];
304 | }
305 |
306 | if(self::$isSandboxed)
307 | {
308 | $routeMiddleware = isset(self::$context['middleware']['route'][0]) ? self::$context['middleware']['route'][0] : [];
309 | self::$context['middleware']['route'][0] = !is_array($routeMiddleware) ? $middleware : array_merge($routeMiddleware, $middleware);
310 | return;
311 | }
312 |
313 | foreach ($middleware as $_middleware) {
314 | if (! in_array($_middleware, self::$context['middleware']['global'])) {
315 | self::$context['middleware']['global'][] = $_middleware;
316 | }
317 | }
318 | }
319 | }
320 |
321 | /**
322 | * {@inheritDoc}
323 | *
324 | * @see \Luthier\Routing\RouteBuilderInterface::getRoutes()
325 | */
326 | public function getRoutes(): RouteCollection
327 | {
328 | foreach ($this->routes as $i => $luthierRoute) {
329 | [$name,$route] = $luthierRoute->compile();
330 |
331 | if (empty($name)) {
332 | $name = '__unnamed_route_' . str_pad($i, 3, '0', STR_PAD_LEFT);
333 | } else {
334 | if (isset($this->names[$name])) {
335 | throw new \Exception("Duplicated '$name' route");
336 | }
337 | $this->names[$name] = $luthierRoute->getStickyParams();
338 | }
339 |
340 | $this->routeCollection->add($name, $route);
341 | }
342 |
343 | $this->routeGenerator = new UrlGenerator($this->routeCollection, $this->requestContext);
344 |
345 | return $this->routeCollection;
346 | }
347 |
348 | /**
349 | * Gets the defined application commands
350 | *
351 | * @return Command[]
352 | */
353 | public function getCommands()
354 | {
355 | return $this->commands;
356 | }
357 |
358 | /**
359 | * Gets the current Route Builder context
360 | *
361 | * @param string $name Context index
362 | *
363 | * @return array|array[]
364 | */
365 | public static function getContext(string $name)
366 | {
367 | return self::$context[$name];
368 | }
369 |
370 | /**
371 | * {@inheritDoc}
372 | *
373 | * @see \Luthier\Routing\RouteBuilderInterface::getRouteByName()
374 | */
375 | public function getRouteByName(string $name, array $args = [], bool $absoluteUrl = true): string
376 | {
377 | $route = $this->currentRoute;
378 |
379 | if (! isset($this->names[$name])) {
380 | throw new \Exception("Undefined \"$name\" route");
381 | }
382 |
383 | foreach ($this->names[$name] as $stickyParam) {
384 | if ($route->hasParam($stickyParam)) {
385 | $args[$stickyParam] = $route->param($stickyParam);
386 | }
387 | }
388 |
389 | $generated = $this->routeGenerator->generate($name, $args, $absoluteUrl ? UrlGeneratorInterface::ABSOLUTE_URL : NULL);
390 |
391 | // If the APP_URL property is set, we'll generate the URLs based on that
392 | // value:
393 | $baseUrl = $this->container->get('APP_URL');
394 | if (!empty($baseUrl)) {
395 | $context = $this->routeGenerator->getContext();
396 | $offset = strpos($generated, $context->getBaseUrl());
397 | $segments = str_ireplace($context->getBaseUrl(), '', substr($generated, $offset));
398 | return $baseUrl . $segments;
399 | }
400 |
401 | // If not, Symfony will generate the URL for us:
402 | return $generated;
403 | }
404 |
405 | /**
406 | * Gets the current Symfony Routing RequestContext object
407 | *
408 | * @return \Symfony\Component\Routing\RequestContext
409 | */
410 | public function getRequestContext()
411 | {
412 | return $this->requestContext;
413 | }
414 |
415 | /**
416 | * Returns a valid middleware callable from the given parameter
417 | *
418 | * @param callable|string|object $middleware
419 | * @throws \Exception
420 | *
421 | * @return callable
422 | */
423 | public function getMiddleware($middleware)
424 | {
425 | if (is_callable($middleware)) {
426 | if ($middleware instanceof \Closure) {
427 | $container = $this->container;
428 | $middleware = \Closure::bind($middleware, $container, ContainerInterface::class);
429 | }
430 |
431 | return $middleware;
432 | }
433 |
434 | if (is_string($middleware)) {
435 | if (isset(self::$context['middleware']['alias'][$middleware])) {
436 | return self::getMiddleware(self::$context['middleware']['alias'][$middleware]);
437 | }
438 |
439 | if (class_exists($middleware)) {
440 | $middleware = new $middleware($this->container);
441 | } else {
442 | throw new \Exception("Unknown \"$middleware\" middleware class/alias");
443 | }
444 | }
445 |
446 | if (! $middleware instanceof MiddlewareInterface) {
447 | throw new \Exception('The middleware "' . get_class($middleware) . '" MUST implement the ' . MiddlewareInterface::class . ' interface');
448 | }
449 |
450 | return function ($request, $response, $next) use ($middleware) {
451 | return $middleware->run($request, $response, $next);
452 | };
453 | }
454 |
455 | /**
456 | * @param \Closure $callback
457 | *
458 | * @return \Closure
459 | */
460 | private function bindContainer(\Closure $callback)
461 | {
462 | return \Closure::bind($callback, $this->container, ContainerInterface::class);
463 | }
464 |
465 | /**
466 | * Sets the callback to be invoked when a HTTP 404 error occurs
467 | *
468 | * @param callable $callback
469 | *
470 | * @return \Luthier\Routing\RouteBuilder
471 | */
472 | public function setHttpNotFoundCallback(\Closure $callback)
473 | {
474 | $this->httpNotFoundCallback = $this->bindContainer($callback);
475 | return $this;
476 | }
477 |
478 | /**
479 | * Sets the callback to be invoked when a HTTP 405 error ocurrs
480 | *
481 | * @param callable $callback
482 | *
483 | * @return \Luthier\Routing\RouteBuilder
484 | */
485 | public function setMethodHttpNotAllowedCallback(\Closure $callback)
486 | {
487 | $this->httpMethodNotAllowedCallback = $this->bindContainer($callback);
488 | return $this;
489 | }
490 |
491 | /**
492 | * Sets the callback to be invoked when a error/exception occurs
493 | *
494 | * @param callable $callback
495 | *
496 | * @return self
497 | */
498 | public function setErrorHandler(\Closure $callback)
499 | {
500 | $this->errorHandler = $this->bindContainer($callback);
501 | return $this;
502 | }
503 |
504 | /**
505 | * @param LuthierRoute $route
506 | *
507 | * @return self
508 | */
509 | public function setCurrentRoute(LuthierRoute $route)
510 | {
511 | $this->currentRoute = $route;
512 | return $this;
513 | }
514 |
515 | /**
516 | * Gets the httpNotFoundCallback property
517 | *
518 | * @return callable|null
519 | */
520 | public function getHttpNotFoundCallback()
521 | {
522 | return $this->httpNotFoundCallback;
523 | }
524 |
525 | /**
526 | * Gets the httpMethodNotAllowedCallback property
527 | *
528 | * @return callable|null
529 | */
530 | public function getHttpMethodNotAllowedCallback()
531 | {
532 | return $this->httpMethodNotAllowedCallback;
533 | }
534 |
535 | /**
536 | * Gets the Error Handler callback
537 | *
538 | * @return callable|null
539 | */
540 | public function getErrorHandler()
541 | {
542 | return $this->errorHandler;
543 | }
544 |
545 | /**
546 | * Gets an array of all middleware registered for current request
547 | *
548 | * @return array
549 | */
550 | public function getMiddlewareStack(LuthierRoute $route)
551 | {
552 | $routeMiddleware = $route->getMiddleware() ?? [];
553 | $globalMiddleware = self::$context['middleware']['global'];
554 | return array_merge($globalMiddleware, $routeMiddleware);
555 | }
556 |
557 | /**
558 | * Counts the defined routes
559 | *
560 | * @return int
561 | */
562 | public function count()
563 | {
564 | return count($this->routes);
565 | }
566 |
567 | /**
568 | * Adds an external routing file. The $router variable will be available to define new routes within that file.
569 | *
570 | * @param string $filename External file
571 | * @param string $prefix Global prefix
572 | * @param array $attributes Global attributes
573 | */
574 | public function addRoutes(string $filename, string $prefix = '', array $attributes = [])
575 | {
576 | self::$isSandboxed = true;
577 |
578 | $appPath = $this->container->get('APP_PATH');
579 |
580 | (function($router) use($appPath, $filename, $prefix, $attributes){
581 | $router->group($prefix, $attributes, function() use($router, $appPath, $filename){
582 | require($appPath . '/' . $filename . (substr($filename, -4) == '.php' ? '' : '.php') );
583 | });
584 | })($this);
585 |
586 | array_pop(self::$context['middleware']['route']);
587 |
588 | self::$isSandboxed = false;
589 | }
590 | }
--------------------------------------------------------------------------------
/src/Routing/RouteBuilderInterface.php:
--------------------------------------------------------------------------------
1 |
18 | */
19 | interface RouteBuilderInterface
20 | {
21 |
22 | /**
23 | * Returns a Symfony RouteCollection object with all compiled Luthier routes
24 | *
25 | * @return RouteCollection
26 | */
27 | public function getRoutes(): RouteCollection;
28 |
29 | /**
30 | * Gets a route URL by its name, or throws an exception if an undefined route was requested
31 | *
32 | * @param string $name Route name
33 | * @param array $args Route parameters
34 | * @param bool $absoluteUrl Build an absolute url
35 | * @throws \Exception
36 | *
37 | * @return string
38 | */
39 | public function getRouteByName(string $name, array $args = [], bool $absoluteUrl = true): string;
40 | }
--------------------------------------------------------------------------------
/src/Routing/RouteParam.php:
--------------------------------------------------------------------------------
1 |
18 | */
19 | final class RouteParam
20 | {
21 |
22 | /**
23 | * @var string
24 | */
25 | private $name;
26 |
27 | /**
28 | * Actual parameter regex
29 | *
30 | * @var string
31 | */
32 | private $regex;
33 |
34 | /**
35 | * @var string
36 | */
37 | private $placeholder;
38 |
39 | /**
40 | * Original segment of the parameter
41 | *
42 | * @var string
43 | */
44 | private $segment;
45 |
46 | /**
47 | * @var bool
48 | */
49 | private $optional;
50 |
51 | /**
52 | * @var bool
53 | */
54 | private $sticky;
55 |
56 | /**
57 | * @var mixed
58 | */
59 | public $value;
60 |
61 | /**
62 | * Parameter segment -> placeholder conversion array
63 | *
64 | * @var string[]
65 | */
66 | private static $placeholderPatterns = [
67 | '{num:[a-zA-Z0-9-_]*(\?}|})' => '(:num)', // (:num) route
68 | '{any:[a-zA-Z0-9-_]*(\?}|})' => '(:any)', // (:any) route
69 | '{[a-zA-Z0-9-_]*(\?}|})' => '(:any)' // Everything else
70 | ];
71 |
72 | /**
73 | * Parameter placeholder -> regex conversion array
74 | *
75 | * @var string[]
76 | */
77 | private static $placeholderReplacements = [
78 | '/\(:any\)/' => '[^/]+',
79 | '/\(:num\)/' => '[0-9]+'
80 | ];
81 |
82 | /**
83 | * Gets the placeholder -> regex conversion array
84 | *
85 | * @return string[]
86 | */
87 | public static function getPlaceholderReplacements()
88 | {
89 | return self::$placeholderReplacements;
90 | }
91 |
92 | /**
93 | * @param string $segment Route segment
94 | */
95 | public function __construct(string $segment)
96 | {
97 | $this->segment = $segment;
98 |
99 | $matches = [];
100 |
101 | if (preg_match('/{\((.*)\):[a-zA-Z0-9-_]*(\?}|})/', $segment, $matches)) {
102 | $this->placeholder = $matches[1];
103 | $this->regex = $matches[1];
104 | $name = preg_replace('/\((.*)\):/', '', $segment, 1);
105 | } else {
106 | foreach (self::$placeholderPatterns as $regex => $replacement) {
107 | $parsedSegment = preg_replace('/' . $regex . '/', $replacement, $segment);
108 |
109 | if ($segment != $parsedSegment) {
110 | $this->placeholder = $replacement;
111 | $this->regex = preg_replace(array_keys(self::$placeholderReplacements), array_values(self::$placeholderReplacements), $replacement, 1);
112 | $name = preg_replace(['/num:/','/any:/'], '', $segment, 1);
113 | break;
114 | }
115 | }
116 | }
117 |
118 | $this->optional = substr($segment, - 2, 1) == '?';
119 | $this->name = substr($name, 1, ! $this->optional ? - 1 : - 2);
120 | $this->sticky = substr($this->name, 0, 1) == '_';
121 | }
122 |
123 | /**
124 | * Gets the segment name
125 | *
126 | * @return string
127 | */
128 | public function getName()
129 | {
130 | return $this->name;
131 | }
132 |
133 | /**
134 | * Gets the original segment string
135 | *
136 | * @return string
137 | */
138 | public function getSegment()
139 | {
140 | return $this->segment;
141 | }
142 |
143 | /**
144 | * Gets the segment regex
145 | *
146 | * @return string
147 | */
148 | public function getRegex()
149 | {
150 | return $this->regex;
151 | }
152 |
153 | /**
154 | * Gets the segment placeholder
155 | *
156 | * @return string
157 | */
158 | public function getPlaceholder()
159 | {
160 | return $this->placeholder;
161 | }
162 |
163 | /**
164 | * Checks if the segment is optional
165 | *
166 | * @return bool
167 | */
168 | public function isOptional()
169 | {
170 | return $this->optional;
171 | }
172 |
173 | /**
174 | * Checks if the segment is sticky
175 | *
176 | * @return bool
177 | */
178 | public function isSticky()
179 | {
180 | return $this->sticky;
181 | }
182 | }
--------------------------------------------------------------------------------
/src/Templating/Driver/BladeDriver.php:
--------------------------------------------------------------------------------
1 |
24 | */
25 | class BladeDriver implements TemplateDriverInterface
26 | {
27 |
28 | /**
29 | * @var ContainerInterface
30 | */
31 | protected $container;
32 |
33 | /**
34 | * @var \Illuminate\Container\Container
35 | */
36 | protected $illuminateContainer;
37 |
38 | /**
39 | * @var array
40 | */
41 | protected $globals = [];
42 |
43 | /**
44 | * @var array
45 | */
46 | protected $functions = [];
47 |
48 | /**
49 | * @var array
50 | */
51 | protected $directories = [];
52 |
53 | /**
54 | * @var \Illuminate\View\Factory
55 | */
56 | protected $blade;
57 |
58 | /**
59 | * @var \Illuminate\View\Engines\CompilerEngine
60 | */
61 | protected $engine;
62 |
63 | /**
64 | * @var bool
65 | */
66 | protected $booted = false;
67 |
68 | /**
69 | * @param ContainerInterface $container
70 | */
71 | public function __construct(ContainerInterface $container)
72 | {
73 | $this->container = $container;
74 | $this->directories[] = $container->get('APP_PATH') . '/' . $container->get('TEMPLATE_DIR');
75 | }
76 |
77 | private function boot()
78 | {
79 | if ($this->booted) {
80 | return;
81 | }
82 | $this->booted = true;
83 | $config = [];
84 |
85 | $directories = $this->directories;
86 | $cache = ! empty($this->container->get('APP_CACHE')) ? $this->container->get('APP_PATH') . '/' . $this->container->get('APP_CACHE') . '/templates' : false;
87 |
88 | if (! file_exists($cache)) {
89 | mkdir($config['cache'], null, true);
90 | }
91 |
92 | $illuminateContainer = new Container();
93 |
94 | $illuminateContainer->bindIf('files', function () {
95 | return new Filesystem();
96 | }, true);
97 |
98 | $illuminateContainer->bindIf('events', function () {
99 | return new Dispatcher();
100 | }, true);
101 |
102 | $illuminateContainer->bindIf('config', function () use ($directories, $cache) {
103 | return ['view.paths' => $directories,'view.compiled' => $cache];
104 | }, true);
105 |
106 | (new ViewServiceProvider($illuminateContainer))->register();
107 |
108 | $this->illuminateContainer = $illuminateContainer;
109 | $this->blade = $this->illuminateContainer['view'];
110 | $this->engine = $this->illuminateContainer->make('view.engine.resolver')->resolve('blade');
111 |
112 | $instance = &$this;
113 |
114 | //
115 | // "$_" variable
116 | //
117 | // Lambda function used for invoke other registered functions
118 | // for this template engine.
119 | //
120 | $this->globals['_'] = function ($name, ...$args) use ($instance) {
121 | if (isset($instance->functions[$name])) {
122 | return $instance->functions[$name](...$args);
123 | }
124 | throw new \Exception("Call to undefined template function $name()");
125 | };
126 | }
127 |
128 | /**
129 | * {@inheritDoc}
130 | *
131 | * @see \Luthier\Templating\Driver\TemplateDriverInterface::addFunction()
132 | */
133 | public function addFunction(string $name, callable $callback, bool $rawHtml = false)
134 | {
135 | $this->functions[$name] = $callback;
136 | }
137 |
138 | /**
139 | * {@inheritDoc}
140 | *
141 | * @see \Luthier\Templating\Driver\TemplateDriverInterface::addGlobal()
142 | */
143 | public function addGlobal(string $name, $value)
144 | {
145 | $this->globals[$name] = $value;
146 | }
147 |
148 | /**
149 | * {@inheritDoc}
150 | *
151 | * @see \Luthier\Templating\Driver\TemplateDriverInterface::addFilter()
152 | */
153 | public function addFilter(string $name, callable $callback, bool $rawHtml = false)
154 | {
155 | $this->addFunction($name, $callback);
156 | }
157 |
158 | /**
159 | * {@inheritDoc}
160 | *
161 | * @see \Luthier\Templating\Driver\TemplateDriverInterface::addDirectory()
162 | */
163 | public function addDirectory(string $dir)
164 | {
165 | $this->directories[] = $dir;
166 | }
167 |
168 | /**
169 | * {@inheritDoc}
170 | *
171 | * @see \Luthier\Templating\Driver\TemplateDriverInterface::render()
172 | */
173 | public function render(string $template, array $vars = [], bool $return = false)
174 | {
175 | $this->boot();
176 |
177 | $view = $this->blade->make($template, array_merge($vars, $this->globals), [])->render();
178 |
179 | if (! $return) {
180 | $this->container->get('response')->write($view);
181 | }
182 |
183 | return $view;
184 | }
185 | }
--------------------------------------------------------------------------------
/src/Templating/Driver/PlainPhpDriver.php:
--------------------------------------------------------------------------------
1 |
20 | */
21 | class PlainPhpDriver implements TemplateDriverInterface
22 | {
23 |
24 | /**
25 | * @var ContainerInterface
26 | */
27 | protected $container;
28 |
29 | /**
30 | * @var array
31 | */
32 | protected $globals = [];
33 |
34 | /**
35 | * @var array
36 | */
37 | protected $functions = [];
38 |
39 | /**
40 | * @var array
41 | */
42 | protected $directories = [];
43 |
44 | /**
45 | * @var array
46 | */
47 | protected $blocks = [];
48 |
49 | /**
50 | * @var mixed
51 | */
52 | protected $parentBlock;
53 |
54 | /**
55 | * @var array
56 | */
57 | protected $extending = [];
58 |
59 | /**
60 | * @var self
61 | */
62 | protected static $instance;
63 |
64 | public static function __callStatic($method, $args)
65 | {
66 | if (isset(self::$instance->functions[$method])) {
67 |
68 | return call_user_func_array(self::$instance->functions[$method], $args);
69 |
70 | } else if ($method == 'extends') {
71 |
72 | if (count($args) < 1) {
73 | throw new \InvalidArgumentException("The 'extends' function expects at least 2 arguments, " . count($args) . " provided");
74 | }
75 |
76 | $template = $args[0];
77 | $vars = $args[1] ?? [];
78 |
79 | self::$instance->extending[] = [$template,$vars];
80 |
81 | } else if ($method == 'block') {
82 |
83 | if (count($args) < 1) {
84 | throw new \InvalidArgumentException("The 'block' function expects at least 2 arguments, " . count($args) . " provided");
85 | }
86 |
87 | $name = $args[0];
88 | $content = $args[1] ?? null;
89 |
90 | if (! isset(self::$instance->blocks[$name])) {
91 | self::$instance->blocks[$name] = $content;
92 | } else {
93 | if (is_callable(self::$instance->blocks[$name])) {
94 | return call_user_func(self::$instance->blocks[$name]);
95 | }
96 |
97 | return self::$instance->blocks[$name];
98 | }
99 |
100 | } else {
101 | throw new \BadMethodCallException('Undefined "' . $method . '" template function');
102 | }
103 | }
104 |
105 | /**
106 | * @param ContainerInterface $container
107 | */
108 | public function __construct(ContainerInterface $container)
109 | {
110 | $this->container = $container;
111 | $this->directories[] = $container->get('APP_PATH') . '/' . $container->get('TEMPLATE_DIR');
112 | $this->registerFunctions();
113 | self::$instance = &$this;
114 | }
115 |
116 | private function registerFunctions()
117 | {
118 | $instance = &$this;
119 |
120 | //
121 | // "$_" variable
122 | //
123 | // Lambda function used for invoke other registered functions
124 | // for this template engine.
125 | //
126 | $this->globals['_'] = function ($name, ...$args) use ($instance) {
127 | if (isset($instance->functions[$name])) {
128 | return $instance->functions[$name](...$args);
129 | }
130 | throw new \Exception("Call to undefined template function $name()");
131 | };
132 |
133 | //
134 | // "$_b" variable
135 | //
136 | // Lambda function used for define and set blocks within the templates
137 | //
138 | $this->globals['_b'] = function ($name, $content = null) use ($instance) {
139 | if (! isset($instance->blocks[$name])) {
140 | if ($content !== null) {
141 | return $instance->blocks[$name] = $content;
142 | }
143 | } else {
144 | if (is_callable($instance->blocks[$name])) {
145 | return call_user_func($instance->blocks[$name]);
146 | }
147 | return $instance->blocks[$name];
148 | }
149 | };
150 |
151 | //
152 | // "$_e" variable
153 | //
154 | // Lambda function used to extend another templates
155 | //
156 | $this->globals['_e'] = function ($template, $vars = []) use ($instance) {
157 | $instance->extending[] = [$template,$vars];
158 | };
159 | }
160 |
161 | /**
162 | * {@inheritDoc}
163 | *
164 | * @see \Luthier\Templating\Driver\TemplateDriverInterface::render()
165 | */
166 | public function render(string $template, array $vars = [], bool $return = false)
167 | {
168 | $filename = null;
169 |
170 | foreach ($this->directories as $dir) {
171 | $match = $dir . '/' . $template . (! substr($template, - 4) != '.php' ? '.php' : '');
172 | if (file_exists($match)) {
173 | $filename = $match;
174 | break;
175 | }
176 | }
177 |
178 | if ($filename === null) {
179 | throw new \Exception("Unable to find template file '$template' (Looked at " . implode(', ', $this->directories) . '")');
180 | }
181 |
182 | extract(array_merge($vars, $this->globals));
183 |
184 | ob_start();
185 | require $filename;
186 | $view = ob_get_clean();
187 |
188 | $extending = array_shift($this->extending);
189 |
190 | if (! empty($extending)) {
191 | [$template,$vars] = $extending;
192 | return $this->render($template, $vars, $return);
193 | }
194 |
195 | if ($return) {
196 | return $view;
197 | }
198 |
199 | $this->container->get('response')->write($view);
200 | }
201 |
202 | /**
203 | * {@inheritDoc}
204 | *
205 | * @see \Luthier\Templating\Driver\TemplateDriverInterface::addFunction()
206 | */
207 | public function addFunction(string $name, callable $callback, bool $rawHtml = false)
208 | {
209 | $this->functions[$name] = $callback;
210 | }
211 |
212 | /**
213 | * {@inheritDoc}
214 | *
215 | * @see \Luthier\Templating\Driver\TemplateDriverInterface::addFilter()
216 | */
217 | public function addFilter(string $name, callable $callback, bool $rawHtml = false)
218 | {
219 | $this->addFunction($name, $callback);
220 | }
221 |
222 | /**
223 | * {@inheritDoc}
224 | *
225 | * @see \Luthier\Templating\Driver\TemplateDriverInterface::addGlobal()
226 | */
227 | public function addGlobal(string $name, $value)
228 | {
229 | $this->globals[$name] = $value;
230 | }
231 |
232 | /**
233 | * {@inheritDoc}
234 | *
235 | * @see \Luthier\Templating\Driver\TemplateDriverInterface::addDirectory()
236 | */
237 | public function addDirectory(string $dir)
238 | {
239 | $this->directories[] = $dir;
240 | }
241 | }
242 |
--------------------------------------------------------------------------------
/src/Templating/Driver/TemplateDriverInterface.php:
--------------------------------------------------------------------------------
1 |
18 | */
19 | interface TemplateDriverInterface
20 | {
21 |
22 | /**
23 | * Renders a template
24 | *
25 | * @param string $template
26 | * @param array $vars
27 | * @param bool $return
28 | *
29 | * @return Response
30 | */
31 | public function render(string $template, array $vars = [], bool $return = false);
32 |
33 | /**
34 | * Registers a global function
35 | *
36 | * @param string $name Function name/alias
37 | * @param callable $callback Function callback
38 | * @param bool $rawHtml Set if this function will return raw (unescaped HTML output)
39 | *
40 | * @return void
41 | */
42 | public function addFunction(string $name, callable $callback, bool $rawHtml = false);
43 |
44 | /**
45 | * Registers a global filter (If the template engine does not support this feature, must
46 | * be emulated)
47 | *
48 | * @param string $name
49 | * @param callable $callback
50 | * @param bool $rawHtml
51 | *
52 | * @return void
53 | */
54 | public function addFilter(string $name, callable $callback, bool $rawHtml = false);
55 |
56 | /**
57 | * Registers a global variable
58 | *
59 | * @param string $name
60 | * @param mixed $value
61 | */
62 | public function addGlobal(string $name, $value);
63 |
64 | /**
65 | * Adds a directory to search templates
66 | *
67 | * @param string $dir
68 | */
69 | public function addDirectory(string $dir);
70 | }
--------------------------------------------------------------------------------
/src/Templating/Driver/TwigDriver.php:
--------------------------------------------------------------------------------
1 | container = $container;
44 | $this->loader = new \Twig_Loader_Filesystem([$container->get('APP_PATH') . '/' . $container->get('TEMPLATE_DIR')]);
45 |
46 | $config = [];
47 | $config['cache'] = ! empty($container->get('APP_CACHE')) ? $container->get('APP_PATH') . '/' . $container->get('APP_CACHE') . '/templates' : false;
48 | $config['debug'] = $container->get('APP_ENV') === 'development';
49 |
50 | $this->twig = new \Twig_Environment($this->loader, $config);
51 | }
52 |
53 | /**
54 | * {@inheritDoc}
55 | *
56 | * @see \Luthier\Templating\Driver\TemplateDriverInterface::addFunction()
57 | */
58 | public function addFunction(string $name, callable $callback, bool $rawHtml = false)
59 | {
60 | $this->twig->addFunction(new \Twig_Function($name, $callback, ! $rawHtml ? [] : ['is_safe' => ['html']]));
61 | }
62 |
63 | /**
64 | * {@inheritDoc}
65 | *
66 | * @see \Luthier\Templating\Driver\TemplateDriverInterface::addGlobal()
67 | */
68 | public function addGlobal(string $name, $value)
69 | {
70 | $this->twig->addGlobal($name, $value);
71 | }
72 |
73 | /**
74 | * {@inheritDoc}
75 | *
76 | * @see \Luthier\Templating\Driver\TemplateDriverInterface::addFilter()
77 | */
78 | public function addFilter(string $name, callable $callback, bool $rawHtml = false)
79 | {
80 | $this->twig->addFilter(new \Twig_Filter($name, $callback, ! $rawHtml ? [] : ['is_safe' => ['html']]));
81 | }
82 |
83 | /**
84 | * {@inheritDoc}
85 | *
86 | * @see \Luthier\Templating\Driver\TemplateDriverInterface::addDirectory()
87 | */
88 | public function addDirectory(string $dir)
89 | {
90 | $this->loader->addPath($dir);
91 | }
92 |
93 | /**
94 | * {@inheritDoc}
95 | *
96 | * @see \Luthier\Templating\Driver\TemplateDriverInterface::render()
97 | */
98 | public function render(string $template, array $vars = [], bool $return = false)
99 | {
100 | if (! substr($template, - 10) != '.html.twig') {
101 | $template .= '.html.twig';
102 | }
103 |
104 | $output = $this->twig->render($template, $vars);
105 |
106 | if (! $return) {
107 | $this->container->get('response')->write($output);
108 | }
109 |
110 | return $output;
111 | }
112 | }
--------------------------------------------------------------------------------
/src/Templating/Template.php:
--------------------------------------------------------------------------------
1 |
22 | */
23 | class Template
24 | {
25 |
26 | /**
27 | * @var TemplateDriverInterface
28 | */
29 | protected $driver;
30 |
31 | /**
32 | * @var ContainerInterface
33 | */
34 | private $container;
35 |
36 | /**
37 | * @param ContainerInterface $container
38 | */
39 | public function __construct(ContainerInterface $container)
40 | {
41 | $this->container = $container;
42 |
43 | $driver = $container->get('TEMPLATE_DRIVER') ?? 'default';
44 |
45 | // PLEASE NOTE: Only the (default) Plain PHP driver works out-of-the-box
46 | // with Luthier Framework. In order to use the other drivers you
47 | // MUST download the respective template engine with Composer.
48 | $defaultDrivers = ['default' => \Luthier\Templating\Driver\PlainPhpDriver::class,'twig' => \Luthier\Templating\Driver\TwigDriver::class,'blade' => \Luthier\Templating\Driver\BladeDriver::class];
49 |
50 | if (isset($defaultDrivers[$driver])) {
51 | $driver = $defaultDrivers[$driver];
52 | }
53 |
54 | $this->driver = new $driver($container);
55 |
56 | $this->configure();
57 | }
58 |
59 | /**
60 | * Configures the template engine, providing a consistent behavior across engines
61 | */
62 | private function configure()
63 | {
64 | if (! ($this->driver instanceof TemplateDriverInterface)) {
65 | throw new \Exception("The " . get_class($this->driver) . '" MUST implement the ' . TemplateDriverInterface::class . ' interface');
66 | }
67 |
68 | // Globals variables
69 | $this->driver->addGlobal('request', $this->container->get('request'));
70 | $this->driver->addGlobal('app', $this->container);
71 |
72 | // Built-in functions
73 | //
74 | // (This is a compatibility layer between plain PHP and some compiled
75 | // php template engines such Twig)
76 | $container = $this->container;
77 |
78 | $this->driver->addFunction('route', function ($name, $args = []) {
79 | return call_user_func_array('route', [$name,$args]);
80 | });
81 |
82 | $this->driver->addFunction('url', function ($url = '') use ($container) {
83 | return call_user_func_array([$this->container->get('request'),'baseUrl'], [$url]);
84 | });
85 |
86 | $this->driver->addFunction('validation_errors', function($field = null) use($container){
87 | return call_user_func_array([$container->get('validator'), 'getValidationErrors'], [$field]);
88 | });
89 |
90 | $this->driver->addFunction('csrf_field', function() use($container){
91 | $tokenName = $container->get('security')->getCsrfTokenName();
92 | $tokenHash = $container->get('security')->getCsrfTokenHash();
93 | if (empty($tokenName) || empty($tokenHash)) {
94 | return;
95 | }
96 | return '';
97 | }, true);
98 | }
99 |
100 | public function __call($method, $args)
101 | {
102 | if (method_exists($this->driver, $method)) {
103 | return call_user_func_array([$this->driver,$method], $args);
104 | }
105 | throw new \BadMethodCallException('Call to undefined method ' . get_class($this->driver) . '::' . $method);
106 | }
107 | }
108 |
--------------------------------------------------------------------------------
/src/UtilsTrait.php:
--------------------------------------------------------------------------------
1 | isXmlHttpRequest()) {
48 | return new JsonResponse(['error' => $message], $status);
49 | } else {
50 | ob_start();
51 | require __DIR__ . '/Resources/Views/Error.php';
52 | $responseBody = ob_get_clean();
53 | return new Response($responseBody, $status);
54 | }
55 | }
56 | }
--------------------------------------------------------------------------------
/src/Validator/Exception/ValidationConstraintException.php:
--------------------------------------------------------------------------------
1 |
19 | */
20 | class ValidationConstraintException extends \Exception
21 | {
22 | }
--------------------------------------------------------------------------------
/src/Validator/Validator.php:
--------------------------------------------------------------------------------
1 |
26 | */
27 | class Validator
28 | {
29 | const OPERATORS = ['>=', '<=', '>', '<', '===', '==', '!==', '!='];
30 |
31 | /**
32 | * @var ContainerInterface
33 | */
34 | protected $container;
35 |
36 | /**
37 | * @var \Symfony\Component\Validator\Validator\ValidatorInterface
38 | */
39 | protected $validator;
40 |
41 | /**
42 | * @var \Symfony\Component\Translation\Translator
43 | */
44 | protected $translator;
45 |
46 | /**
47 | * @var array
48 | */
49 | protected $rules = [];
50 |
51 | /**
52 | * @var array
53 | */
54 | protected $validationErrors = [];
55 |
56 | /**
57 | * @param ContainerInterface $container
58 | */
59 | public function __construct(ContainerInterface $container)
60 | {
61 | $this->container = $container;
62 | $locale = $this->container->get('APP_LANG');
63 | $translator = $this->container->get('translator');
64 | $translator->addLoader('xlf', new XliffFileLoader());
65 | $translator->addResource('xlf', realpath( __DIR__ . '/../../vendor/symfony/validator/Resources/translations/validators.' . $locale . '.xlf'), $locale, "validators");
66 | $this->translator = $translator;
67 |
68 | $this->reset(true);
69 | }
70 |
71 | /**
72 | * Resets the validator
73 | *
74 | * @param bool $preserveErrors Preserve the validation errors?
75 | *
76 | * @return void
77 | */
78 | public function reset(bool $preserveErrors = false)
79 | {
80 | $validator = Validation::createValidatorBuilder();
81 | $validator->setTranslator($this->translator);
82 | $validator->setTranslationDomain('validators');
83 |
84 | $this->validator = $validator->getValidator();
85 | $this->rules = [];
86 |
87 | if(!$preserveErrors) {
88 | $this->validationErrors = [];
89 | }
90 | }
91 |
92 | /**
93 | * Adds a new validation rule to the validator
94 | *
95 | * @param string $field
96 | * @param string|array $constraints
97 | * @param string[] $messages
98 | *
99 | * @return self
100 | */
101 | public function rule(string $field, $constraints, array $messages = [])
102 | {
103 | if(!is_string($constraints) && !is_array($constraints)){
104 | throw new \InvalidArgumentException("Validation constraints must be declared as a string or an array");
105 | }
106 |
107 | $this->rules[] = [
108 | $field,
109 | $constraints,
110 | $messages
111 | ];
112 | return $this;
113 | }
114 |
115 | /**
116 | * @return \Symfony\Component\Validator\Validator\ValidatorInterface
117 | */
118 | public function getValidator()
119 | {
120 | return $this->validator;
121 | }
122 |
123 | /**
124 | * Runs the validator
125 | *
126 | * Returns TRUE if no constraints violations occurred, FALSE instead
127 | *
128 | * @param array $data Data to be validated
129 | * @param array $constraints Constraints
130 | * @param bool $bailAll Stop on first constraint error on each field?
131 | *
132 | * @return bool
133 | */
134 | public function validate($data = [], array $constraints = [], bool $bailAll = false)
135 | {
136 | if(!is_array($data) && (!($data instanceof Request))){
137 | throw new \InvalidArgumentException('Validation data must be an array or an instance of ' . Request::class);
138 | }
139 |
140 | if($data instanceof Request){
141 | $data = $data->getRequest()->request->all();
142 | }
143 |
144 | $validationErrors = [];
145 |
146 | if (!empty($constraints)) {
147 | foreach ($constraints as $field => $validation) {
148 | if (is_array($validation)) {
149 | $rules = $validation[0];
150 | $messages = $validation[1] ?? [];
151 | } else {
152 | $rules = $validation;
153 | $messages = [];
154 | }
155 | $this->rule($field,$rules,$messages);
156 | }
157 | }
158 |
159 | foreach ($this->rules as [
160 | $field,
161 | $rules,
162 | $messages
163 | ]) {
164 | $this->messages = $messages;
165 | $required = false;
166 | $matches = null;
167 | $nullable = false;
168 | $sfRules = [];
169 | $bail = $bailAll;
170 | $error = false;
171 |
172 | if (is_string($rules)) {
173 | $rules = explode('|', $rules);
174 | }
175 |
176 | foreach ($rules as $name => $params) {
177 | if(is_string($params) && is_int($name)){
178 | // String based constraint definition:
179 | $name = $params;
180 | if (count(explode(':', $name)) > 1) {
181 | [
182 | $name,
183 | $params
184 | ] = explode(':', $name);
185 | $params = explode(',', $params);
186 | } else {
187 |
188 | $isOperator = false;
189 |
190 | foreach(self::OPERATORS as $operator){
191 | if(count(explode($operator, $name)) > 1){
192 | $params = explode($operator, $name);
193 | $params = [ $params[1] ];
194 | $name = $operator;
195 | $isOperator = true;
196 | break;
197 | }
198 | }
199 |
200 | if(!$isOperator){
201 | $params = [];
202 | }
203 | }
204 | } else {
205 | // Array based constraint definition:
206 | if (is_scalar($params) || is_array($params)) {
207 | if (is_scalar($params)) {
208 | $params = [$params];
209 | }
210 | } else {
211 | $params = [];
212 | }
213 | }
214 |
215 | // Special 'required' pseudo-constraint
216 | if ($name === 'required') {
217 | $required = true;
218 | continue;
219 | }
220 |
221 | // Special 'matches' pseudo-constraint
222 | if ($name === 'matches' && isset($params[0]) && is_string($params[0])){
223 | $matches = $params[0];
224 | continue;
225 | }
226 |
227 | // Is a nullable field?
228 | if ($name === 'nullable') {
229 | $nullable = true;
230 | continue;
231 | }
232 |
233 | // The validation must stop at first error?
234 | if ($name === 'bail') {
235 | $bail = true;
236 | continue;
237 | }
238 |
239 | // Get the actual Symfony Constraint object
240 | $sfRules[] = $this->getSfConstraint($name, $params, $messages[$name] ?? null);
241 | }
242 |
243 | if ($required && !$nullable) {
244 | $sfRules[] = $this->getSfConstraint('not_blank', [], $messages[$name] ?? null);
245 | $sfRules[] = $this->getSfConstraint('not_null', [], $messages[$name] ?? null);
246 | }
247 |
248 | $violations = new ConstraintViolationList();
249 |
250 | if ($required && ! isset($data[$field])) {
251 | $error = true;
252 | $errorMessage = $messages['required'] ?? "This field is missing.";
253 | $translatedError = $this->translator->trans($errorMessage, [], "validators");
254 | $violations->add(new ConstraintViolation(
255 | $translatedError,
256 | $translatedError,
257 | [],
258 | null,
259 | null,
260 | null,
261 | null,
262 | null,
263 | new Constraints\Required())
264 | );
265 | }
266 |
267 | if ($matches !== null && (!isset($data[$matches]) || !isset($data[$field]) || $data[$matches] != $data[$field]) && (!$bail || ($bail && !$error))) {
268 | $error = true;
269 | $errorMessage = $messages['matches'] ?? "This value should be equal to {{ compared_value }}.";
270 | $violations->add(new ConstraintViolation(
271 | $this->translator->trans($errorMessage, ["{{ compared_value }}" => '[' . $matches . ']'], "validators"),
272 | $this->translator->trans($errorMessage, [], "validators"),
273 | [
274 | "{{ value }}" => $data[$field] ?? null,
275 | "{{ compared_value }}" => $data[$matches] ?? null,
276 | "{{ compared_value_type }}" => "string"
277 | ],
278 | $data[$field] ?? null,
279 | null,
280 | $data[$field] ?? null,
281 | null,
282 | null,
283 | new Constraints\EqualTo($matches))
284 | );
285 | }
286 |
287 | if(!$bail || ($bail && !$error))
288 | {
289 | foreach ($sfRules as $sfRule) {
290 | $violation = $this->validator->validate($data[$field] ?? null, $sfRule);
291 | $violations->addAll($violation);
292 | if (count($violation) > 0 && $bail) {
293 | break;
294 | }
295 | }
296 | }
297 |
298 | foreach ($violations as $violation) {
299 | $validationErrors[$field][] = $violation->getMessage();
300 | }
301 | }
302 |
303 | $this->validationErrors = $validationErrors;
304 |
305 | return empty($validationErrors);
306 | }
307 |
308 | /**
309 | * Same as validate(), but throws an exception if a constraint violation
310 | * occurs
311 | *
312 | * @param array $data Data to be validated
313 | * @param array $constraints Constraints
314 | * @param bool $bailAll Stop on first constraint error on each field?
315 | *
316 | * @throws \Exception
317 | *
318 | * @return void
319 | */
320 | public function validateOrFail($data = [], array $constraints = [], bool $bailAll = false)
321 | {
322 | if($this->validate($data, $constraints, $bailAll) !== true){
323 | throw new Exception\ValidationConstraintException("One or more constraint violations occurred with the submited data");
324 | }
325 | }
326 |
327 | /**
328 | * Gets the validation error list
329 | *
330 | * @param string $field Specific field name
331 | *
332 | * @return array
333 | */
334 | public function getValidationErrors(string $field = null)
335 | {
336 | return $field !== null
337 | ? $this->validationErrors[$field] ?? []
338 | : $this->validationErrors;
339 | }
340 |
341 | /**
342 | * Sets the validation errors list
343 | *
344 | * @param array $errors
345 | */
346 | public function setValidationErrors(array $errors)
347 | {
348 | $this->validationErrors = $errors;
349 | }
350 |
351 | /**
352 | * Compiles the given rule and parameters to a Symfony validator's constraint
353 | *
354 | * @param string $name
355 | * @param array $params
356 | * @param string $message
357 | *
358 | * @return \Symfony\Component\Validator\Constraint
359 | */
360 | private function getSfConstraint(string $name, array $params, ?string $message)
361 | {
362 | $constraint = null;
363 |
364 | // Constraint instances
365 | switch ($name) {
366 | /*
367 | * (Not) Blank validation
368 | */
369 | case 'not_blank':
370 | $constraint = new Constraints\NotBlank();
371 | break;
372 | /*
373 | * (Not) Null validation
374 | */
375 | case 'not_null':
376 | $constraint = new Constraints\NotNull();
377 | break;
378 | /*
379 | * Regex validation
380 | */
381 | case 'regex':
382 | $constraint = new Constraints\Regex([
383 | 'pattern' => $params[0]
384 | ]);
385 | break;
386 | /*
387 | * String lenght validation (min)
388 | */
389 | case 'min_length':
390 | $constraint = new Constraints\Length([
391 | 'min' => (int) $params[0]
392 | ]);
393 | break;
394 | /*
395 | * String lenght validation (max)
396 | */
397 | case 'max_length':
398 | $constraint = new Constraints\Length([
399 | 'max' => (int) $params[0]
400 | ]);
401 | break;
402 | /*
403 | * String collection ('enum' like) validation
404 | */
405 | case 'in_list':
406 | $constraint = new Constraints\Choice($params);
407 | break;
408 | /*
409 | * Email validation
410 | */
411 | case 'email':
412 | $constraint = new Constraints\Email();
413 | break;
414 | /*
415 | * Numeric integer validation
416 | */
417 | case 'integer':
418 | $constraint = new Constraints\Regex([
419 | 'pattern' => '/^[\-+]?[0-9]+$/'
420 | ]);
421 | break;
422 | /*
423 | * Numeric decimal validation
424 | */
425 | case 'decimal':
426 | $constraint = new Constraints\Regex([
427 | 'pattern' => '/^[\-+]?[0-9]*[\.,][0-9]+$/'
428 | ]);
429 | break;
430 | /*
431 | * Numeric validation (both integer and decimal)
432 | */
433 | case 'number':
434 | $constraint = new Constraints\Regex([
435 | 'pattern' => '/^[\-+]?([0-9]+|[0-9]*[\.,][0-9]+)$/'
436 | ]);
437 | break;
438 | /*
439 | * Numeric range validation
440 | */
441 | case 'in_range':
442 | $constraint = new Constraints\Range([
443 | 'min' => (int) $params[0],
444 | 'max' => (int) $params[1],
445 | ]);
446 | break;
447 | /*
448 | * Numeric Greater Than or Equal (>=) validation
449 | */
450 | case 'greater_than_equal_to':
451 | case 'gte':
452 | case '>=':
453 | $constraint = new Constraints\GreaterThanOrEqual($params[0]);
454 | break;
455 | /*
456 | * Numeric Greater Than (>) validation
457 | */
458 | case 'greater_than':
459 | case 'gt':
460 | case '>':
461 | $constraint = new Constraints\GreaterThan($params[0]);
462 | break;
463 | /*
464 | * Numeric Lesser Than or Equal (<=) validation
465 | */
466 | case 'less_than_equal_to':
467 | case 'lte':
468 | case '<=':
469 | $constraint = new Constraints\LessThanOrEqual($params[0]);
470 | break;
471 | /*
472 | * Numeric Lesser Than (<) validation
473 | */
474 | case 'less_than':
475 | case 'lt':
476 | case '<':
477 | $constraint = new Constraints\LessThan($params[0]);
478 | break;
479 | /*
480 | * String identical (===) validation
481 | */
482 | case 'identical':
483 | case '===':
484 | $constraint = new Constraints\IdenticalTo($params[0]);
485 | break;
486 | /*
487 | * String equal (==) validation
488 | */
489 | case 'equal':
490 | case '==':
491 | $constraint = new Constraints\EqualTo($params[0]);
492 | break;
493 | /*
494 | * String not identical (!==) validation
495 | */
496 | case 'not_identical':
497 | case '!==':
498 | $constraint = new Constraints\NotIdenticalTo($params[0]);
499 | break;
500 | /*
501 | * String not equal (!=) validation
502 | */
503 | case 'not_equal':
504 | case '!=':
505 | $constraint = new Constraints\NotEqualTo($params[0]);
506 | break;
507 | default:
508 | throw new \Exception('Unknown validation rule "' . $name . '"');
509 | }
510 |
511 | // Translated error messages
512 | switch ($name) {
513 | case 'range':
514 | $translatedMessage = $this->translator->trans(
515 | !empty($message) ? $message : $constraint->minMessage,
516 | [],
517 | "validators"
518 | );
519 | $constraint->minMessage = $translatedMessage;
520 | $constraint->maxMessage = $translatedMessage;
521 | break;
522 | case 'min_length':
523 | $constraint->minMessage = $this->translator->trans(
524 | !empty($message) ? $message : $constraint->minMessage,
525 | [],
526 | "validators"
527 | );
528 | break;
529 | case 'max_length':
530 | $constraint->maxMessage = $this->translator->trans(
531 | !empty($message) ? $message : $constraint->maxMessage,
532 | [],
533 | "validators"
534 | );
535 | break;
536 | default:
537 | $constraint->message = $this->translator->trans(
538 | !empty($message) ? $message : $constraint->message,
539 | [],
540 | "validators"
541 | );
542 | }
543 |
544 | return $constraint;
545 | }
546 | }
--------------------------------------------------------------------------------
/tests/ConfigurationTest.php:
--------------------------------------------------------------------------------
1 | 'development']);
13 | $envDirInstance = new Configuration(null, __DIR__ . '/assets');
14 |
15 | $this->assertSame(
16 | array_keys(Configuration::getDefaultConfig()),
17 | array_keys($defaultInstance->parse())
18 | );
19 |
20 | $this->assertSame(
21 | array_keys(Configuration::getDefaultConfig()),
22 | array_keys($configArrayInstance->parse())
23 | );
24 |
25 | $this->assertSame(
26 | array_keys(Configuration::getDefaultConfig()),
27 | array_keys($envDirInstance->parse())
28 | );
29 | }
30 |
31 | public function testEnvDirNullValues()
32 | {
33 | $instance = new Configuration(null, __DIR__ . '/assets');
34 | foreach(Configuration::getDefaultConfig() as $name => $default)
35 | {
36 | $this->assertSame($default, $instance->parse()[$name]);
37 | }
38 | }
39 |
40 | public function testConfigurationPrecedence()
41 | {
42 | $instance = new Configuration(['APP_ENV' => 'production'], __DIR__ . '/assets');
43 | $this->assertSame($instance->parse()['APP_ENV'], 'development');
44 | }
45 |
46 | public function testInexistentEnvFileException()
47 | {
48 | $this->expectException(\Exception::class);
49 | $instance = new Configuration(null, __DIR__ . '/inexistent');
50 | $instance->parse();
51 | }
52 |
53 | }
--------------------------------------------------------------------------------
/tests/ContainerTest.php:
--------------------------------------------------------------------------------
1 | service('router', \Luthier\Routing\RouteBuilder::class);
22 |
23 | foreach([$defaultInstance, $customInstance] as $container)
24 | {
25 | foreach(Container::getDefaultContainer() as $name => $service)
26 | {
27 | if(!$container->has($name))
28 | {
29 | $container->service($name, $service);
30 | }
31 | }
32 | }
33 |
34 | $this->assertSame(
35 | array_keys(Container::getDefaultContainer()),
36 | $defaultInstance->getServices()
37 | );
38 |
39 | $this->assertSame(
40 | array_keys(Container::getDefaultContainer()),
41 | $customInstance->getServices()
42 | );
43 | }
44 | }
--------------------------------------------------------------------------------
/tests/FrameworkTest.php:
--------------------------------------------------------------------------------
1 | assertInstanceOf(Routing\Route::class, $instance->__call($verb, ['foo', function(){}]));
26 | }
27 |
28 | // Multiple HTTP verb route definition
29 | $this->assertInstanceOf(Routing\Route::class, $instance->__call('match', [[], 'foo', function(){}]));
30 |
31 | // Route group
32 | foreach(
33 | [
34 | /* Short group syntax */
35 | ['prefix', function(){}],
36 | /* Extended group syntax */
37 | ['prefix', ['middleware' => [], 'namespace' => ''], function(){} ],
38 | ['prefix', ['middleware' => []], function(){} ],
39 | ['prefix', ['namespace' => ''], function(){} ],
40 | ]
41 | as $syntax)
42 | {
43 | $this->assertNull($instance->__call('group', $syntax));
44 | }
45 |
46 | // Route middleware
47 | foreach(
48 | [
49 | 'foo', /* Running a middleware alias */
50 | function(){}, /* Running a middleware closure */
51 | ['foo','bar','baz'], /* Running an array of middleware aliases */
52 | [
53 | function(){},
54 | function(){},
55 | function(){}
56 | ], /* Running an array of middleware closures */
57 | ['foo', function(){}], /* Running an array of middleware closures/aliases */
58 | ['foo' => 'bar' ], /* Defining a middleware alias of a callable */
59 | ['foo' => function(){} ], /* Defining a middleware alias of a closure */
60 | ]
61 | as $syntax
62 | )
63 | {
64 | $this->assertNull($instance->__call('middleware', [$syntax]));
65 | }
66 | }
67 | }
68 |
69 |
--------------------------------------------------------------------------------
/tests/assets/.env:
--------------------------------------------------------------------------------
1 | # .env
2 | APP_ENV=development
3 | APP_NAME=Luthier
4 | APP_URL=
5 | APP_KEY=
6 |
--------------------------------------------------------------------------------