1: <?php
2:
3: namespace Charcoal\App;
4:
5: use Exception;
6: use InvalidArgumentException;
7:
8: // From PSR-7
9: use Psr\Http\Message\UriInterface;
10:
11: // From Slim
12: use Slim\Http\Uri;
13:
14: // From 'charcoal-config'
15: use Charcoal\Config\AbstractConfig;
16:
17: // From 'charcoal-view'
18: use Charcoal\View\ViewConfig;
19:
20: // From 'charcoal-app'
21: use Charcoal\App\Config\CacheConfig;
22: use Charcoal\App\Config\FilesystemConfig;
23: use Charcoal\App\Config\LoggerConfig;
24:
25: /**
26: * Charcoal App configuration
27: */
28: class AppConfig extends AbstractConfig
29: {
30: /**
31: * The application's timezone.
32: *
33: * @var string|null
34: */
35: private $timezone;
36:
37: /**
38: * The application's name.
39: *
40: * For internal usage.
41: *
42: * @var string|null
43: */
44: private $projectName;
45:
46: /**
47: * The base path for the Charcoal installation.
48: *
49: * @var string|null
50: */
51: private $basePath;
52:
53: /**
54: * The base URL (public) for the Charcoal installation.
55: *
56: * @var UriInterface|null
57: */
58: private $baseUrl;
59:
60: /**
61: * The path to the public / web directory.
62: *
63: * @var string|null
64: */
65: private $publicPath;
66:
67: /**
68: * The path to the storage directory.
69: *
70: * @var string|null
71: */
72: private $storagePath;
73:
74: /**
75: * Whether the debug mode is enabled (TRUE) or not (FALSE).
76: *
77: * @var boolean
78: */
79: private $devMode = false;
80:
81: /**
82: * The application's routes.
83: *
84: * @var array
85: */
86: private $routes = [];
87:
88: /**
89: * The application's dynamic routes.
90: *
91: * @var array
92: */
93: private $routables = [];
94:
95: /**
96: * The application's handlers.
97: *
98: * @var array
99: */
100: private $handlers = [];
101:
102: /**
103: * The application's modules.
104: *
105: * @var array
106: */
107: private $modules = [];
108:
109: /**
110: * The application's caching configset.
111: *
112: * @var CacheConfig
113: */
114: private $cache;
115:
116: /**
117: * The application's logging configset.
118: *
119: * @var LoggerConfig
120: */
121: private $logger;
122:
123: /**
124: * The application's view/rendering configset.
125: *
126: * @var ViewConfig
127: */
128: protected $view;
129:
130: /**
131: * The application's database configsets.
132: *
133: * @var array
134: */
135: private $databases = [];
136:
137: /**
138: * The application's default database configset.
139: *
140: * @var string
141: */
142: private $defaultDatabase;
143:
144: /**
145: * The application's filesystem configset.
146: *
147: * @var FilesystemConfig
148: */
149: private $filesystem;
150:
151: /**
152: * Default app-config values.
153: *
154: * @return array
155: */
156: public function defaults()
157: {
158: /** @var string $baseDir Presume that Charcoal App _is_ the application */
159: $baseDir = rtrim(realpath(__DIR__.'/../../../'), '/').'/';
160:
161: return [
162: 'project_name' => '',
163: 'base_path' => $baseDir,
164: 'public_path' => null,
165: 'storage_path' => null,
166: 'timezone' => 'UTC',
167: 'routes' => [],
168: 'routables' => [],
169: 'handlers' => [],
170: 'modules' => [],
171: 'cache' => [],
172: 'logger' => [],
173: 'view' => [],
174: 'databases' => [],
175: 'default_database' => 'default',
176: 'dev_mode' => false
177: ];
178: }
179:
180: /**
181: * Set the application's absolute root path.
182: *
183: * Resolves symlinks with realpath() and ensure trailing slash.
184: *
185: * @param string $path The absolute path to the application's root directory.
186: * @throws InvalidArgumentException If the argument is not a string.
187: * @return AppConfig Chainable
188: */
189: public function setBasePath($path)
190: {
191: if ($path === null) {
192: throw new InvalidArgumentException(
193: 'The base path is required.'
194: );
195: }
196:
197: if (!is_string($path)) {
198: throw new InvalidArgumentException(
199: 'The base path must be a string'
200: );
201: }
202:
203: $this->basePath = rtrim(realpath($path), '\\/').DIRECTORY_SEPARATOR;
204:
205: return $this;
206: }
207:
208: /**
209: * Retrieve the application's absolute root path.
210: *
211: * @return string The absolute path to the application's root directory.
212: */
213: public function basePath()
214: {
215: return $this->basePath;
216: }
217:
218: /**
219: * Set the application's absolute path to the public web directory.
220: *
221: * @param string $path The path to the application's public directory.
222: * @throws InvalidArgumentException If the argument is not a string.
223: * @return AppConfig Chainable
224: */
225: public function setPublicPath($path)
226: {
227: if ($path === null) {
228: $this->publicPath = null;
229:
230: return $this;
231: }
232:
233: if (!is_string($path)) {
234: throw new InvalidArgumentException(
235: 'The public path must be a string'
236: );
237: }
238:
239: $this->publicPath = rtrim(realpath($path), '\\/').DIRECTORY_SEPARATOR;
240:
241: return $this;
242: }
243:
244: /**
245: * Retrieve the application's absolute path to the public web directory.
246: *
247: * @return string The absolute path to the application's public directory.
248: */
249: public function publicPath()
250: {
251: if (!isset($this->publicPath)) {
252: return $this->basePath().'www'.DIRECTORY_SEPARATOR;
253: }
254:
255: return $this->publicPath;
256: }
257:
258: /**
259: * Set the application's absolute path to the storage directory.
260: *
261: * @param string|null $path The path to the application's storage directory.
262: * @throws InvalidArgumentException If the argument is not a string.
263: * @return $this
264: */
265: public function setStoragePath($path)
266: {
267: if ($path === null) {
268: $this->storagePath = null;
269:
270: return $this;
271: }
272:
273: if (!is_string($path)) {
274: throw new InvalidArgumentException(
275: 'The storage path must be a string'
276: );
277: }
278:
279: $this->storagePath = rtrim(realpath($path), '\\/').DIRECTORY_SEPARATOR;
280:
281: return $this;
282: }
283:
284: /**
285: * Get the path to the storage directory.
286: *
287: * Note that the storage space is outside of the public access.
288: *
289: * @return string
290: */
291: public function storagePath()
292: {
293: if (!isset($this->storagePath)) {
294: return $this->basePath().'uploads'.DIRECTORY_SEPARATOR;
295: }
296:
297: return $this->storagePath;
298: }
299:
300: /**
301: * Set the application's fully qualified base URL to the public web directory.
302: *
303: * @param UriInterface|string $uri The base URI to the application's web directory.
304: * @return AppConfig Chainable
305: */
306: public function setBaseUrl($uri)
307: {
308: $this->baseUrl = Uri::createFromString($uri);
309:
310: return $this;
311: }
312:
313: /**
314: * Retrieve the application's fully qualified base URL to the public web directory.
315: *
316: * @return UriInterface|null The base URI to the application's web directory.
317: */
318: public function baseUrl()
319: {
320: return $this->baseUrl;
321: }
322:
323: /**
324: * Set the application's default timezone.
325: *
326: * @param string $timezone The timezone string.
327: * @throws InvalidArgumentException If the argument is not a string.
328: * @return AppConfig Chainable
329: */
330: public function setTimezone($timezone)
331: {
332: if (!is_string($timezone)) {
333: throw new InvalidArgumentException(
334: 'Timezone must be a string.'
335: );
336: }
337: $this->timezone = $timezone;
338: return $this;
339: }
340:
341: /**
342: * Retrieve the application's default timezone.
343: *
344: * Will be used by the PHP date and date-time functions.
345: *
346: * @return string
347: */
348: public function timezone()
349: {
350: if (isset($this->timezone)) {
351: return $this->timezone;
352: } else {
353: return 'UTC';
354: }
355: }
356:
357: /**
358: * Sets the project name.
359: *
360: * @param string|null $projectName The project name.
361: * @throws InvalidArgumentException If the project argument is not a string (or null).
362: * @return AppConfig Chainable
363: */
364: public function setProjectName($projectName)
365: {
366: if ($projectName === null) {
367: $this->projectName = null;
368: return $this;
369: }
370: if (!is_string($projectName)) {
371: throw new InvalidArgumentException(
372: 'Project name must be a string'
373: );
374: }
375: $this->projectName = $projectName;
376: return $this;
377: }
378:
379: /**
380: * @return string|null
381: */
382: public function projectName()
383: {
384: if ($this->projectName === null) {
385: $baseUrl = $this->baseUrl();
386: if ($baseUrl) {
387: return $baseUrl->getHost();
388: }
389: }
390: return $this->projectName;
391: }
392:
393: /**
394: * @param boolean $devMode The "dev mode" flag.
395: * @return AppConfig Chainable
396: */
397: public function setDevMode($devMode)
398: {
399: $this->devMode = !!$devMode;
400: return $this;
401: }
402:
403: /**
404: * @return boolean
405: */
406: public function devMode()
407: {
408: return !!$this->devMode;
409: }
410:
411: /**
412: * @param array $view The view configuration structure to set.
413: * @return AppConfig Chainable
414: */
415: public function setView(array $view)
416: {
417: if (!isset($this->view)) {
418: $this->view = [];
419: }
420:
421: $this->view = array_merge($this->view, $view);
422:
423: return $this;
424: }
425:
426: /**
427: * Parse the application's route configuration.
428: *
429: * @see \Charcoal\Admin\Config::setRoutes() For a similar implementation.
430: * @param array $routes The route configuration structure to set.
431: * @return AppConfig Chainable
432: */
433: public function setRoutes(array $routes)
434: {
435: if (!isset($this->routes)) {
436: $this->routes = [];
437: }
438:
439: $toIterate = [ 'templates', 'actions', 'scripts' ];
440: foreach ($routes as $key => $val) {
441: if (in_array($key, $toIterate) && isset($this->routes[$key])) {
442: $this->routes[$key] = array_merge($this->routes[$key], $val);
443: } else {
444: $this->routes[$key] = $val;
445: }
446: }
447:
448: return $this;
449: }
450:
451: /**
452: * @return array
453: */
454: public function routes()
455: {
456: return $this->routes;
457: }
458:
459: /**
460: * @param array $routables The routable configuration structure to set.
461: * @return AppConfig Chainable
462: */
463: public function setRoutables(array $routables)
464: {
465: $this->routables = $routables;
466: return $this;
467: }
468:
469: /**
470: * @return array
471: */
472: public function routables()
473: {
474: return $this->routables;
475: }
476:
477: /**
478: * Define custom response and error handlers.
479: *
480: * Slim provides five standard handlers:
481: * - "foundHandler"
482: * - "notFoundHandler"
483: * - "notAllowedHandler"
484: * - "errorHandler"
485: * - "phpErrorHandler"
486: *
487: * @param array $handlers The handlers configuration structure to set.
488: * @return AppConfig Chainable
489: */
490: public function setHandlers(array $handlers)
491: {
492: $this->handlers = $handlers;
493: return $this;
494: }
495:
496: /**
497: * @return array
498: */
499: public function handlers()
500: {
501: return $this->handlers;
502: }
503:
504: /**
505: * Set the configuration modules.
506: *
507: * The modules are defined in a `key`=>`\Charcoal\App\Module\ModuleConfig` structure.
508: *
509: * @param array $modules The module configuration structure to set.
510: * @return AppConfig Chainable
511: */
512: public function setModules(array $modules)
513: {
514: $this->modules = $modules;
515: return $this;
516: }
517:
518: /**
519: * @return array
520: */
521: public function modules()
522: {
523: return $this->modules;
524: }
525:
526: /**
527: * @param array|CacheConfig $cache The application global cache config.
528: * @throws InvalidArgumentException If the argument is not an array or a config.
529: * @return AppConfig Chainable
530: */
531: public function setCache($cache)
532: {
533: if ($cache instanceof CacheConfig) {
534: $this->cache = $cache;
535: $this->cache->addDelegate($this);
536: } elseif (is_array($cache)) {
537: $this->cache = new CacheConfig($cache, [$this]);
538: } else {
539: throw new InvalidArgumentException(
540: 'Cache must be an array of config options or a CacheConfig object.'
541: );
542: }
543: return $this;
544: }
545:
546: /**
547: * Get the application's global `CacheConfig`.
548: *
549: * @return CacheConfig
550: */
551: public function cache()
552: {
553: return $this->cache;
554: }
555:
556: /**
557: * @param array|LoggerConfig $logger The global logger config.
558: * @throws InvalidArgumentException If the argument is not an array or a config.
559: * @return AppConfig Chainable
560: */
561: public function setLogger($logger)
562: {
563: if ($logger instanceof LoggerConfig) {
564: $this->logger = $logger;
565: $this->logger->addDelegate($this);
566: } elseif (is_array($logger)) {
567: $this->logger = new LoggerConfig($logger, [$this]);
568: } else {
569: throw new InvalidArgumentException(
570: 'Logger must be an array of config options or a LoggerConfig object.'
571: );
572: }
573: return $this;
574: }
575:
576: /**
577: * Get the application's global `LoggerConfig`
578: *
579: * @return LoggerConfig
580: */
581: public function logger()
582: {
583: return $this->logger;
584: }
585:
586: /**
587: * @param array $databases The avaiable databases config.
588: * @return Config Chainable
589: */
590: public function setDatabases(array $databases)
591: {
592: $this->databases = $databases;
593: return $this;
594: }
595:
596: /**
597: * @throws Exception If trying to access this method and no databases were set.
598: * @return array
599: */
600: public function databases()
601: {
602: if ($this->databases === null) {
603: throw new Exception(
604: 'Invalid app config: Databases are not set.'
605: );
606: }
607: return $this->databases;
608: }
609:
610: /**
611: * @param string $ident The ident of the database to return the configuration of.
612: * @throws InvalidArgumentException If the ident argument is not a string.
613: * @throws Exception If trying to access an invalid database.
614: * @return array
615: */
616: public function databaseConfig($ident)
617: {
618: if (!is_string($ident)) {
619: throw new InvalidArgumentException(
620: 'Invalid app config: default database must be a string.'
621: );
622: }
623: $databases = $this->databases();
624: if (!isset($databases[$ident])) {
625: throw new Exception(
626: sprintf('Invalid app config: no database configuration matches "%s".', $ident)
627: );
628: }
629: return $databases[$ident];
630: }
631:
632: /**
633: * @param string $defaultDatabase The default database ident.
634: * @throws InvalidArgumentException If the argument is not a string.
635: * @return AppConfig Chainable
636: */
637: public function setDefaultDatabase($defaultDatabase)
638: {
639: if (!is_string($defaultDatabase)) {
640: throw new InvalidArgumentException(
641: 'Invalid app config: Default database must be a string.'
642: );
643: }
644: $this->defaultDatabase = $defaultDatabase;
645: return $this;
646: }
647:
648: /**
649: * @param string $ident The database ident.
650: * @param array $config The database options.
651: * @throws InvalidArgumentException If the arguments are invalid.
652: * @return AppConfig Chainable
653: */
654: public function addDatabase($ident, array $config)
655: {
656: if (!is_string($ident)) {
657: throw new InvalidArgumentException(
658: 'Invalid app config: database ident must be a string.'
659: );
660: }
661:
662: if ($this->databases === null) {
663: $this->databases = [];
664: }
665: $this->databases[$ident] = $config;
666: return $this;
667: }
668:
669: /**
670: * @throws Exception If trying to access this method before a setter.
671: * @return mixed
672: */
673: public function defaultDatabase()
674: {
675: if ($this->defaultDatabase === null) {
676: throw new Exception(
677: 'Invalid app config: default database is not set.'
678: );
679: }
680: return $this->defaultDatabase;
681: }
682:
683: /**
684: * @param array|FilesystemConfig $filesystem The application global cache config.
685: * @throws InvalidArgumentException If the argument is not an array or a config.
686: * @return AppConfig Chainable
687: */
688: public function setFilesystem($filesystem)
689: {
690: if ($filesystem instanceof FilesystemConfig) {
691: $this->filesystem = $filesystem;
692: $this->filesystem->addDelegate($this);
693: } elseif (is_array($filesystem)) {
694: $this->filesystem = new FileSystemConfig($filesystem, [$this]);
695: } else {
696: throw new InvalidArgumentException(
697: 'Filesystem must be an array of config options or a FilesystemConfig object.'
698: );
699: }
700: return $this;
701: }
702:
703: /**
704: * Get the application's global `FilesystemConfig`
705: *
706: * @return FilesystemConfig
707: */
708: public function filesystem()
709: {
710: return $this->filesystem;
711: }
712: }
713: