1: <?php
2:
3: namespace Charcoal\App\ServiceProvider;
4:
5: // Dependencies from Pimple
6: use Pimple\ServiceProviderInterface;
7: use Pimple\Container;
8:
9: // From PSR-7
10: use Psr\Http\Message\UriInterface;
11:
12: // Dependencies from Slim
13: use Slim\Http\Uri;
14:
15: // Dependencies from league/climate
16: use League\CLImate\CLImate;
17:
18: // Dependencies from Mustache
19: use Mustache_LambdaHelper as LambdaHelper;
20:
21: // Dependencies from charcoal-factory
22: use Charcoal\Factory\GenericFactory as Factory;
23:
24: // Intra-module (`charcoal-app`) dependencies
25: use Charcoal\App\Action\ActionInterface;
26: use Charcoal\App\Script\ScriptInterface;
27: use Charcoal\App\Module\ModuleInterface;
28:
29: use Charcoal\App\Route\ActionRoute;
30: use Charcoal\App\Route\RouteInterface;
31: use Charcoal\App\Route\ScriptRoute;
32: use Charcoal\App\Route\TemplateRoute;
33:
34: use Charcoal\App\Handler\Error;
35: use Charcoal\App\Handler\PhpError;
36: use Charcoal\App\Handler\Shutdown;
37: use Charcoal\App\Handler\NotAllowed;
38: use Charcoal\App\Handler\NotFound;
39:
40: use Charcoal\App\Template\TemplateInterface;
41: use Charcoal\App\Template\TemplateBuilder;
42: use Charcoal\App\Template\WidgetInterface;
43: use Charcoal\App\Template\WidgetBuilder;
44:
45: /**
46: * Application Service Provider
47: *
48: * Configures Charcoal and Slim and provides various Charcoal services to a container.
49: *
50: * ## Services
51: * - `logger` `\Psr\Log\Logger`
52: *
53: * ## Helpers
54: * - `logger/config` `\Charcoal\App\Config\LoggerConfig`
55: *
56: * ## Requirements / Dependencies
57: * - `config` A `ConfigInterface` must have been previously registered on the container.
58: */
59: class AppServiceProvider implements ServiceProviderInterface
60: {
61: /**
62: * Registers services on the given container.
63: *
64: * This method should only be used to configure services and parameters.
65: * It should not get services.
66: *
67: * @param Container $container A container instance.
68: * @return void
69: */
70: public function register(Container $container)
71: {
72: $this->registerHandlerServices($container);
73: $this->registerRouteServices($container);
74: $this->registerRequestControllerServices($container);
75: $this->registerScriptServices($container);
76: $this->registerModuleServices($container);
77: $this->registerViewServices($container);
78: }
79:
80: /**
81: * @param Container $container The DI container.
82: * @return void
83: */
84: protected function registerHandlerServices(Container $container)
85: {
86: $config = $container['config'];
87:
88: if (!isset($container['debug'])) {
89: /**
90: * Application Debug Mode
91: *
92: * @param Container $container
93: * @return boolean
94: */
95: $container['debug'] = function (Container $container) {
96: if (isset($container['config']['debug'])) {
97: $debug = !!$container['config']['debug'];
98: } elseif (isset($container['config']['dev_mode'])) {
99: $debug = !!$container['config']['dev_mode'];
100: } else {
101: $debug = false;
102: }
103:
104: return $debug;
105: };
106: }
107:
108: if (!isset($container['base-url'])) {
109: /**
110: * Base URL as a PSR-7 UriInterface object for the current request
111: * or the Charcoal application.
112: *
113: * @param Container $container
114: * @return \Psr\Http\Message\UriInterface
115: */
116: $container['base-url'] = function (Container $container) {
117: if (isset($container['config']['base_url'])) {
118: $baseUrl = $container['config']['base_url'];
119: } else {
120: $baseUrl = $container['request']->getUri()->getBaseUrl();
121: }
122:
123: $baseUrl = Uri::createFromString($baseUrl)->withUserInfo('');
124:
125: /** Fix the base path */
126: $path = $baseUrl->getPath();
127: if ($path) {
128: $baseUrl = $baseUrl->withBasePath($path)->withPath('');
129: }
130:
131: return $baseUrl;
132: };
133: }
134:
135: if (!isset($config['handlers'])) {
136: return;
137: }
138:
139: /**
140: * HTTP 404 (Not Found) handler.
141: *
142: * @param object|HandlerInterface $handler An error handler instance.
143: * @param Container $container A container instance.
144: * @return HandlerInterface
145: */
146: $container->extend('notFoundHandler', function ($handler, Container $container) use ($config) {
147: if ($handler instanceof \Slim\Handlers\NotFound) {
148: $handler = new NotFound($container);
149:
150: if (isset($config['handlers']['notFound'])) {
151: $handler->config()->merge($config['handlers']['notFound']);
152: }
153:
154: $handler->init();
155: }
156:
157: return $handler;
158: });
159:
160: /**
161: * HTTP 405 (Not Allowed) handler.
162: *
163: * @param object|HandlerInterface $handler An error handler instance.
164: * @param Container $container A container instance.
165: * @return HandlerInterface
166: */
167: $container->extend('notAllowedHandler', function ($handler, Container $container) use ($config) {
168: if ($handler instanceof \Slim\Handlers\NotAllowed) {
169: $handler = new NotAllowed($container);
170:
171: if (isset($config['handlers']['notAllowed'])) {
172: $handler->config()->merge($config['handlers']['notAllowed']);
173: }
174:
175: $handler->init();
176: }
177:
178: return $handler;
179: });
180:
181: /**
182: * HTTP 500 (Error) handler for PHP 7+ Throwables.
183: *
184: * @param object|HandlerInterface $handler An error handler instance.
185: * @param Container $container A container instance.
186: * @return HandlerInterface
187: */
188: $container->extend('phpErrorHandler', function ($handler, Container $container) use ($config) {
189: if ($handler instanceof \Slim\Handlers\PhpError) {
190: $handler = new PhpError($container);
191:
192: if (isset($config['handlers']['phpError'])) {
193: $handler->config()->merge($config['handlers']['phpError']);
194: }
195:
196: $handler->init();
197: }
198:
199: return $handler;
200: });
201:
202: /**
203: * HTTP 500 (Error) handler.
204: *
205: * @param object|HandlerInterface $handler An error handler instance.
206: * @param Container $container A container instance.
207: * @return HandlerInterface
208: */
209: $container->extend('errorHandler', function ($handler, Container $container) use ($config) {
210: if ($handler instanceof \Slim\Handlers\Error) {
211: $handler = new Error($container);
212:
213: if (isset($config['handlers']['error'])) {
214: $handler->config()->merge($config['handlers']['error']);
215: }
216:
217: $handler->init();
218: }
219:
220: return $handler;
221: });
222:
223: if (!isset($container['shutdownHandler'])) {
224: /**
225: * HTTP 503 (Service Unavailable) handler.
226: *
227: * This handler is not part of Slim.
228: *
229: * @param Container $container A container instance.
230: * @return HandlerInterface
231: */
232: $container['shutdownHandler'] = function (Container $container) {
233: $config = $container['config'];
234: $handler = new Shutdown($container);
235:
236: if (isset($config['handlers']['shutdown'])) {
237: $handler->config()->merge($config['handlers']['shutdown']);
238: }
239:
240: return $handler->init();
241: };
242: }
243: }
244:
245: /**
246: * @param Container $container The DI container.
247: * @return void
248: */
249: protected function registerRouteServices(Container $container)
250: {
251: /** @var string The default route controller for actions. */
252: $container['route/controller/action/class'] = ActionRoute::class;
253:
254: /** @var string The default route controller for scripts. */
255: $container['route/controller/script/class'] = ScriptRoute::class;
256:
257: /** @var string The default route controller for templates. */
258: $container['route/controller/template/class'] = TemplateRoute::class;
259:
260: /**
261: * The Route Factory service is used to instanciate new routes.
262: *
263: * @param Container $container A container instance.
264: * @return \Charcoal\Factory\FactoryInterface
265: */
266: $container['route/factory'] = function (Container $container) {
267: return new Factory([
268: 'base_class' => RouteInterface::class,
269: 'resolver_options' => [
270: 'suffix' => 'Route'
271: ],
272: 'arguments' => [[
273: 'logger' => $container['logger']
274: ]]
275: ]);
276: };
277: }
278:
279: /**
280: * @param Container $container The DI container.
281: * @return void
282: */
283: protected function registerRequestControllerServices(Container $container)
284: {
285: /**
286: * The Action Factory service is used to instanciate new actions.
287: *
288: * - Actions are `ActionInterface` and must be suffixed with `Action`.
289: * - The container is passed to the created action constructor, which will call `setDependencies()`.
290: *
291: * @param Container $container A container instance.
292: * @return \Charcoal\Factory\FactoryInterface
293: */
294: $container['action/factory'] = function (Container $container) {
295: return new Factory([
296: 'base_class' => ActionInterface::class,
297: 'resolver_options' => [
298: 'suffix' => 'Action'
299: ],
300: 'arguments' => [[
301: 'container' => $container,
302: 'logger' => $container['logger'],
303:
304: ]]
305: ]);
306: };
307:
308: /**
309: * The Script Factory service is used to instanciate new scripts.
310: *
311: * - Scripts are `ScriptInterface` and must be suffixed with `Script`.
312: * - The container is passed to the created script constructor, which will call `setDependencies()`.
313: *
314: * @param Container $container A container instance.
315: * @return \Charcoal\Factory\FactoryInterface
316: */
317: $container['script/factory'] = function (Container $container) {
318: return new Factory([
319: 'base_class' => ScriptInterface::class,
320: 'resolver_options' => [
321: 'suffix' => 'Script'
322: ],
323: 'arguments' => [[
324: 'container' => $container,
325: 'logger' => $container['logger'],
326: 'climate' => $container['climate'],
327: 'climate_reader' => $container['climate/reader']
328: ]]
329: ]);
330: };
331:
332: /**
333: * The Template Factory service is used to instanciate new templates.
334: *
335: * - Templates are `TemplateInterface` and must be suffixed with `Template`.
336: * - The container is passed to the created template constructor, which will call `setDependencies()`.
337: *
338: * @param Container $container A container instance.
339: * @return \Charcoal\Factory\FactoryInterface
340: */
341: $container['template/factory'] = function (Container $container) {
342: return new Factory([
343: 'base_class' => TemplateInterface::class,
344: 'resolver_options' => [
345: 'suffix' => 'Template'
346: ],
347: 'arguments' => [[
348: 'container' => $container,
349: 'logger' => $container['logger']
350: ]]
351: ]);
352: };
353:
354: /**
355: * The Widget Factory service is used to instanciate new widgets.
356: *
357: * - Widgets are `WidgetInterface` and must be suffixed with `Widget`.
358: * - The container is passed to the created widget constructor, which will call `setDependencies()`.
359: *
360: * @param Container $container A container instance.
361: * @return \Charcoal\Factory\FactoryInterface
362: */
363: $container['widget/factory'] = function (Container $container) {
364: return new Factory([
365: 'base_class' => WidgetInterface::class,
366: 'resolver_options' => [
367: 'suffix' => 'Widget'
368: ],
369: 'arguments' => [[
370: 'container' => $container,
371: 'logger' => $container['logger']
372: ]]
373: ]);
374: };
375: /**
376: * @param Container $container A container instance.
377: * @return TemplateBuilder
378: */
379: $container['widget/builder'] = function (Container $container) {
380: return new WidgetBuilder($container['widget/factory'], $container);
381: };
382: }
383:
384: /**
385: * @param Container $container The DI container.
386: * @return void
387: */
388: protected function registerModuleServices(Container $container)
389: {
390: /**
391: * The Module Factory service is used to instanciate new modules.
392: *
393: * - Modules are `ModuleInterface` and must be suffixed with `Module`.
394: *
395: * @param Container $container A container instance.
396: * @return \Charcoal\Factory\FactoryInterface
397: */
398: $container['module/factory'] = function (Container $container) {
399: return new Factory([
400: 'base_class' => ModuleInterface::class,
401: 'resolver_options' => [
402: 'suffix' => 'Module'
403: ],
404: 'arguments' => [[
405: 'logger' => $container['logger']
406: ]]
407: ]);
408: };
409: }
410:
411: /**
412: * @param Container $container A container instance.
413: * @return void
414: */
415: protected function registerScriptServices(Container $container)
416: {
417: /**
418: * @todo Needs implementation
419: * @param Container $container A container instance.
420: * @return null|\League\CLImate\Util\Reader\ReaderInterface
421: */
422: $container['climate/reader'] = function (Container $container) {
423: return null;
424: };
425:
426: /**
427: * @param Container $container A container instance.
428: * @return CLImate
429: */
430: $container['climate'] = function () {
431: $climate = new CLImate();
432: return $climate;
433: };
434: }
435:
436: /**
437: * @param Container $container A container instance.
438: * @return void
439: */
440: protected function registerViewServices(Container $container)
441: {
442: if (!isset($container['view/mustache/helpers'])) {
443: $container['view/mustache/helpers'] = function () {
444: return [];
445: };
446: }
447:
448: /**
449: * Extend helpers for the Mustache Engine
450: *
451: * @return array
452: */
453: $container->extend('view/mustache/helpers', function (array $helpers, Container $container) {
454: $baseUrl = $container['base-url'];
455: $urls = [
456: /**
457: * Application debug mode.
458: *
459: * @return boolean
460: */
461: 'debug' => ($container['config']['debug'] || $container['config']['dev_mode']),
462: /**
463: * Retrieve the base URI of the project.
464: *
465: * @return UriInterface|null
466: */
467: 'siteUrl' => $baseUrl,
468: /**
469: * Alias of "siteUrl"
470: *
471: * @return UriInterface|null
472: */
473: 'baseUrl' => $baseUrl,
474: /**
475: * Prepend the base URI to the given path.
476: *
477: * @param string $uri A URI path to wrap.
478: * @return UriInterface|null
479: */
480: 'withBaseUrl' => function ($uri, LambdaHelper $helper = null) use ($baseUrl) {
481: if ($helper) {
482: $uri = $helper->render($uri);
483: }
484:
485: $uri = strval($uri);
486: if ($uri === '') {
487: $uri = $baseUrl->withPath('');
488: } else {
489: $parts = parse_url($uri);
490: if (!isset($parts['scheme'])) {
491: if (!in_array($uri[0], [ '/', '#', '?' ])) {
492: $path = isset($parts['path']) ? $parts['path'] : '';
493: $query = isset($parts['query']) ? $parts['query'] : '';
494: $hash = isset($parts['fragment']) ? $parts['fragment'] : '';
495:
496: $uri = $baseUrl->withPath($path)
497: ->withQuery($query)
498: ->withFragment($hash);
499: }
500: }
501: }
502:
503: return $uri;
504: }
505: ];
506:
507: return array_merge($helpers, $urls);
508: });
509: }
510: }
511: