Initial commit
This commit is contained in:
commit
b1aa1608c3
7
.gitignore
vendored
Normal file
7
.gitignore
vendored
Normal file
|
|
@ -0,0 +1,7 @@
|
||||||
|
.idea/
|
||||||
|
|
||||||
|
/cache/
|
||||||
|
composer.lock
|
||||||
|
deploy.sh
|
||||||
|
deploy-excludes
|
||||||
|
/vendor/
|
||||||
10
.phpstorm.meta.php
Normal file
10
.phpstorm.meta.php
Normal file
|
|
@ -0,0 +1,10 @@
|
||||||
|
<?php
|
||||||
|
namespace PHPSTORM_META
|
||||||
|
{
|
||||||
|
|
||||||
|
use Musoka\Zap\Application;
|
||||||
|
|
||||||
|
override(Application::get(), map([
|
||||||
|
'router' => Musoka\Zap\Routing\Router::class
|
||||||
|
]));
|
||||||
|
}
|
||||||
14
LICENSE
Normal file
14
LICENSE
Normal file
|
|
@ -0,0 +1,14 @@
|
||||||
|
Copyright 2023-2024 CrocWork
|
||||||
|
|
||||||
|
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated
|
||||||
|
documentation files (the "Software"), to deal in the Software without restriction, including without limitation the
|
||||||
|
rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit
|
||||||
|
persons to whom the Software is furnished to do so, subject to the following conditions:
|
||||||
|
|
||||||
|
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the
|
||||||
|
Software.
|
||||||
|
|
||||||
|
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE
|
||||||
|
WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
|
||||||
|
COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR
|
||||||
|
OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||||
25
composer.json
Normal file
25
composer.json
Normal file
|
|
@ -0,0 +1,25 @@
|
||||||
|
{
|
||||||
|
"name": "crocwork/zappy-base",
|
||||||
|
"description": "PHP microframework for Musoka projects.",
|
||||||
|
"type": "library",
|
||||||
|
"license": "MIT",
|
||||||
|
"autoload": {
|
||||||
|
"files": [
|
||||||
|
"src/helpers.php"
|
||||||
|
],
|
||||||
|
"psr-4": {
|
||||||
|
"Musoka\\Zap\\": "src/"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"authors": [
|
||||||
|
{
|
||||||
|
"name": "Fristi",
|
||||||
|
"email": "support@musoka.network"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"minimum-stability": "stable",
|
||||||
|
"require": {
|
||||||
|
"php": ">=8.1",
|
||||||
|
"symfony/http-foundation": "^6.3"
|
||||||
|
}
|
||||||
|
}
|
||||||
228
src/Application.php
Normal file
228
src/Application.php
Normal file
|
|
@ -0,0 +1,228 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace Musoka\Zap;
|
||||||
|
|
||||||
|
use Musoka\Zap\Components\ComponentAggregator;
|
||||||
|
use Musoka\Zap\Components\ComponentProviderInterface;
|
||||||
|
use Musoka\Zap\Config\ConfigRepository;
|
||||||
|
use Musoka\Zap\ErrorHandling\Handler;
|
||||||
|
use Musoka\Zap\View\Renderable;
|
||||||
|
use Musoka\Zap\Http\Request;
|
||||||
|
use Musoka\Zap\Http\Response;
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Application
|
||||||
|
*
|
||||||
|
* The Application class acts as the main entrypoint for Zap application.
|
||||||
|
* On instantiation, the class will set up required services and routing
|
||||||
|
* for the application.
|
||||||
|
*
|
||||||
|
* This class also acts as an access point for obtaining objects registered
|
||||||
|
* by services. Services will register magic methods on the Application
|
||||||
|
* instance (as well as docblock comments for hinting them) that can be used
|
||||||
|
* to obtain service objects.
|
||||||
|
*/
|
||||||
|
class Application
|
||||||
|
{
|
||||||
|
const VERSION = '0.0.1';
|
||||||
|
|
||||||
|
protected static self $instance;
|
||||||
|
|
||||||
|
protected ComponentAggregator $components;
|
||||||
|
|
||||||
|
protected string $basePath;
|
||||||
|
|
||||||
|
protected array $paths;
|
||||||
|
|
||||||
|
protected string $baseUrl;
|
||||||
|
|
||||||
|
protected Handler $errorHandler;
|
||||||
|
|
||||||
|
protected ConfigRepository $config;
|
||||||
|
|
||||||
|
protected Request $request;
|
||||||
|
|
||||||
|
|
||||||
|
public function __construct(string $basePath = null)
|
||||||
|
{
|
||||||
|
static::$instance = $this;
|
||||||
|
$this->components = new ComponentAggregator();
|
||||||
|
|
||||||
|
//Initialize error handling
|
||||||
|
$this->errorHandler = new Handler(true);
|
||||||
|
$this->errorHandler->register(fn(?\Throwable $e = null) => $this->sendErrorResponse($e));
|
||||||
|
|
||||||
|
//Setup base path
|
||||||
|
if(!$basePath || !file_exists($basePath) || !is_dir($basePath)) {
|
||||||
|
$basePath = implode('/', array_slice(explode('/', $_SERVER['SCRIPT_NAME']), 0, -2)) . '/';
|
||||||
|
}
|
||||||
|
$this->setBasePath($basePath); //TODO: move path management to own service?
|
||||||
|
|
||||||
|
//Setup base bindings and components
|
||||||
|
//Note that config and the request object are manually added, but can be overridden by other providers.
|
||||||
|
$this->config = new ConfigRepository(realpath($this->getConfigPath()), realpath($this->getBasePath('cache')));
|
||||||
|
$this->request = Request::createFromGlobals();
|
||||||
|
|
||||||
|
$this->components->addComponent('app', $this);
|
||||||
|
$this->components->addComponent('config', $this->config);
|
||||||
|
$this->components->addComponent('request', $this->request);
|
||||||
|
|
||||||
|
$this->baseUrl = $this->config->get('application.url') ?? $this->request->getBaseUrl();
|
||||||
|
|
||||||
|
//Discover services and add them to the container
|
||||||
|
//Includes services for translation, routing and the viewfactory.
|
||||||
|
$providers = $this->config->get('application.providers', []);
|
||||||
|
foreach($providers as $providerClass) {
|
||||||
|
try {
|
||||||
|
$provider = new $providerClass();
|
||||||
|
$this->components->addProvider($provider);
|
||||||
|
}
|
||||||
|
catch (\Throwable $e) {
|
||||||
|
print_r($e->getMessage());
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
/*if(class_exists($providerClass) && $providerClass instanceof ComponentProviderInterface) {
|
||||||
|
print_r($providerClass);
|
||||||
|
$provider = new $providerClass();
|
||||||
|
$this->components->addProvider($provider);
|
||||||
|
}*/
|
||||||
|
}
|
||||||
|
$this->components->registerComponents(); //TODO: maybe we should defer initializing components until needed.
|
||||||
|
}
|
||||||
|
|
||||||
|
public static function getInstance(): static
|
||||||
|
{
|
||||||
|
return static::$instance;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getComponents(): ComponentAggregator
|
||||||
|
{
|
||||||
|
return $this->components;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getBasePath(string $path = ''): string
|
||||||
|
{
|
||||||
|
return ($this->paths['base'] ?? $this->basePath) . ($path ? DIRECTORY_SEPARATOR.$path : '');
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getConfigPath(string $path = ''): string
|
||||||
|
{
|
||||||
|
return ($this->paths['config'] ?? $this->getBasePath('config')) . ($path ? DIRECTORY_SEPARATOR.$path : '');
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getLangPath(string $path = ''): string
|
||||||
|
{
|
||||||
|
return ($this->paths['lang'] ?? $this->getResourcePath('i18n')) . ($path ? DIRECTORY_SEPARATOR.$path : '');
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getPublicPath(string $path = ''): string
|
||||||
|
{
|
||||||
|
return ($this->paths['public'] ?? $this->getBasePath('public_html')) . ($path ? DIRECTORY_SEPARATOR.$path : '');
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getStoragePath(string $path = ''): string
|
||||||
|
{
|
||||||
|
return ($this->paths['storage'] ?? $this->getBasePath('storage')) . ($path ? DIRECTORY_SEPARATOR.$path : '');
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getResourcePath(string $path = ''): string
|
||||||
|
{
|
||||||
|
return ($this->paths['resource'] ?? $this->getBasePath('resources')) . ($path ? DIRECTORY_SEPARATOR.$path : '');
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getViewPath(string $path = ''): string
|
||||||
|
{
|
||||||
|
return ($this->paths['view'] ?? $this->getResourcePath('views')) . ($path ? DIRECTORY_SEPARATOR.$path : '');
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getCachePath(string $path = ''): string
|
||||||
|
{
|
||||||
|
return ($this->paths['cache'] ?? $this->getBasePath('cache')) . ($path ? DIRECTORY_SEPARATOR.$path : '');
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getBaseUrl(string $path = ''): string
|
||||||
|
{
|
||||||
|
return $this->baseUrl . (!empty($path) ? DIRECTORY_SEPARATOR.$path : '');
|
||||||
|
}
|
||||||
|
|
||||||
|
public function get(string $id = '')
|
||||||
|
{
|
||||||
|
return $this->components->get($id);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected function setBasePath($basePath): static
|
||||||
|
{
|
||||||
|
$this->basePath = rtrim($basePath, '\/');
|
||||||
|
|
||||||
|
$this->paths = [
|
||||||
|
'base' => $this->getBasePath(),
|
||||||
|
'config' => $this->getConfigPath(),
|
||||||
|
'public' => $this->getPublicPath(),
|
||||||
|
'resources' => $this->getResourcePath(),
|
||||||
|
'storage' => $this->getStoragePath(),
|
||||||
|
'view' => $this->getViewPath(),
|
||||||
|
'lang' => $this->getLangPath(),
|
||||||
|
'cache' => $this->getCachePath(),
|
||||||
|
];
|
||||||
|
|
||||||
|
return $this;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
public function run()
|
||||||
|
{
|
||||||
|
$router = $this->components->get('router');
|
||||||
|
if(!$router) {
|
||||||
|
throw new \RuntimeException('Could not get router instance, aborting.');
|
||||||
|
}
|
||||||
|
|
||||||
|
$response = $router->run();
|
||||||
|
$this->handleResponse($response);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* TODO: move this to the router. */
|
||||||
|
protected function handleResponse($response)
|
||||||
|
{
|
||||||
|
//Response can be anything, ranging from text to views to already complete response objects.
|
||||||
|
if(!is_a($response, Response::class)) {
|
||||||
|
if($response === false) {
|
||||||
|
//Router did not find a route, so 404
|
||||||
|
$this->sendHTTPErrorResponse(404);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if(empty($response)) {
|
||||||
|
$response = new Response('');
|
||||||
|
} elseif (is_a($response, Renderable::class)) {
|
||||||
|
$response = new Response($response->render());
|
||||||
|
} else {
|
||||||
|
$response = new Response((string) $response);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
//If request was HEAD request, only send back headers.
|
||||||
|
if($this->request->getMethod() == 'HEAD') {
|
||||||
|
$response->sendHeaders();
|
||||||
|
} else {
|
||||||
|
$response->send();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
protected function sendHTTPErrorResponse(int $status = 404, ?\Throwable $e, array $headers = [])
|
||||||
|
{
|
||||||
|
$response = new Response(
|
||||||
|
Response::$statusTexts[$status] ?? '' ,
|
||||||
|
$status,
|
||||||
|
$headers
|
||||||
|
);
|
||||||
|
|
||||||
|
$response->send();
|
||||||
|
}
|
||||||
|
|
||||||
|
protected function sendErrorResponse(?\Throwable $e)
|
||||||
|
{
|
||||||
|
$this->sendHTTPErrorResponse(500, $e);
|
||||||
|
}
|
||||||
|
}
|
||||||
18
src/Components/AbstractComponentProvider.php
Normal file
18
src/Components/AbstractComponentProvider.php
Normal file
|
|
@ -0,0 +1,18 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace Musoka\Zap\Components;
|
||||||
|
|
||||||
|
abstract class AbstractComponentProvider implements ComponentProviderInterface
|
||||||
|
{
|
||||||
|
protected array $provides = [];
|
||||||
|
|
||||||
|
public function has(string $componentId): bool
|
||||||
|
{
|
||||||
|
return in_array($componentId, $this->provides);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function provides(): array
|
||||||
|
{
|
||||||
|
return $this->provides;
|
||||||
|
}
|
||||||
|
}
|
||||||
8
src/Components/BootableProviderInterface.php
Normal file
8
src/Components/BootableProviderInterface.php
Normal file
|
|
@ -0,0 +1,8 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace Musoka\Zap\Components;
|
||||||
|
|
||||||
|
interface BootableProviderInterface extends ComponentProviderInterface
|
||||||
|
{
|
||||||
|
|
||||||
|
}
|
||||||
98
src/Components/ComponentAggregator.php
Normal file
98
src/Components/ComponentAggregator.php
Normal file
|
|
@ -0,0 +1,98 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace Musoka\Zap\Components;
|
||||||
|
|
||||||
|
class ComponentAggregator
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* @var ComponentInterface[] List of registered components.
|
||||||
|
*/
|
||||||
|
protected array $components = [];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @var ComponentProviderInterface[] List of component providers.
|
||||||
|
*/
|
||||||
|
protected array $providers = [];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @var string[] List of initialized providers.
|
||||||
|
*/
|
||||||
|
protected array $registered = [];
|
||||||
|
|
||||||
|
|
||||||
|
public function addProvider(ComponentProviderInterface $provider): static
|
||||||
|
{
|
||||||
|
if(!key_exists($provider::class, $this->providers)) {
|
||||||
|
$this->providers[$provider::class] = $provider;
|
||||||
|
}
|
||||||
|
return $this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function addComponent(string $id, object|callable $component, bool $override = false): static
|
||||||
|
{
|
||||||
|
if($override || !key_exists($id, $this->components)) {
|
||||||
|
$this->components[$id] = $component;
|
||||||
|
}
|
||||||
|
return $this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function get(string $componentId)
|
||||||
|
{
|
||||||
|
try {
|
||||||
|
return $this->resolveComponent($componentId);
|
||||||
|
}
|
||||||
|
catch (\Exception $e) {
|
||||||
|
throw $e;
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public function has(string $componentId): bool
|
||||||
|
{
|
||||||
|
return key_exists($componentId, $this->components);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function provides(): array
|
||||||
|
{
|
||||||
|
return array_keys($this->components);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function registerComponents()
|
||||||
|
{
|
||||||
|
foreach($this->providers as $provider) {
|
||||||
|
if(in_array($provider::class, $this->registered)) continue;
|
||||||
|
$provider->registerComponents($this);
|
||||||
|
$this->registered[] = $provider::class;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
protected function resolveComponent(string $componentId)
|
||||||
|
{
|
||||||
|
if(!key_exists($componentId, $this->components)) {
|
||||||
|
foreach($this->providers as $provider) {
|
||||||
|
if($provider->has($componentId)) {
|
||||||
|
$provider->registerComponents($this);
|
||||||
|
|
||||||
|
//Check if provider didn't lie to us.
|
||||||
|
if(!key_exists($componentId, $this->components)) {
|
||||||
|
throw new \Exception('ComponentProvider did not register component.');
|
||||||
|
}
|
||||||
|
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if(!key_exists($componentId, $this->components)) {
|
||||||
|
throw new \Exception('Component unavailable; nothing registers this component.');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$component = $this->components[$componentId];
|
||||||
|
|
||||||
|
if(is_callable($component)) {
|
||||||
|
return call_user_func($component);
|
||||||
|
} else {
|
||||||
|
return $component;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
12
src/Components/ComponentProviderInterface.php
Normal file
12
src/Components/ComponentProviderInterface.php
Normal file
|
|
@ -0,0 +1,12 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace Musoka\Zap\Components;
|
||||||
|
|
||||||
|
interface ComponentProviderInterface
|
||||||
|
{
|
||||||
|
public function has(string $componentId): bool;
|
||||||
|
|
||||||
|
public function provides(): array;
|
||||||
|
|
||||||
|
public function registerComponents(ComponentAggregator $aggregator): void;
|
||||||
|
}
|
||||||
66
src/Config/ConfigRepository.php
Normal file
66
src/Config/ConfigRepository.php
Normal file
|
|
@ -0,0 +1,66 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace Musoka\Zap\Config;
|
||||||
|
|
||||||
|
class ConfigRepository
|
||||||
|
{
|
||||||
|
protected array $items;
|
||||||
|
|
||||||
|
public function __construct(string $configDirPath, string $cacheDirPath)
|
||||||
|
{
|
||||||
|
//Load from cache if it exists
|
||||||
|
if($this->loadFromCache($cacheDirPath)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
//Otherwise, load from config files
|
||||||
|
$this->loadConfigurationFiles($configDirPath);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function get(string $key, $default = null)
|
||||||
|
{
|
||||||
|
$keys = explode('.', $key);
|
||||||
|
|
||||||
|
$value = $this->items;
|
||||||
|
foreach($keys as $index) {
|
||||||
|
if(!key_exists($index, $value)) return $default;
|
||||||
|
$value = $value[$index];
|
||||||
|
}
|
||||||
|
|
||||||
|
return $value;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected function loadFromCache(string $cacheDirPath): bool
|
||||||
|
{
|
||||||
|
if(!file_exists($cacheDirPath) || !file_exists($cacheDirPath . DIRECTORY_SEPARATOR . 'config-cache.php')) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->items = require $cacheDirPath . DIRECTORY_SEPARATOR . 'config-cache.php';
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected function loadConfigurationFiles(string $configDirPath)
|
||||||
|
{
|
||||||
|
$files = glob($configDirPath . DIRECTORY_SEPARATOR . '*.php');
|
||||||
|
|
||||||
|
if($files === false || empty($files)) {
|
||||||
|
throw new \RuntimeException('Unable to load configuration, no configuration files found.');
|
||||||
|
} elseif(!in_array($configDirPath . DIRECTORY_SEPARATOR . 'application.php', $files)) {
|
||||||
|
throw new \RuntimeException('Unable to load the application configuration, app configuration file is missing.');
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->items = [];
|
||||||
|
foreach($files as $file) {
|
||||||
|
$items = require $file;
|
||||||
|
$key = basename($file, '.php');
|
||||||
|
$this->items[$key] = $items;
|
||||||
|
//$this->items = array_merge($this->items, $items);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
protected function writeToCache()
|
||||||
|
{
|
||||||
|
// TODO: Write caching function for configuration
|
||||||
|
}
|
||||||
|
}
|
||||||
156
src/DIContainer.php
Normal file
156
src/DIContainer.php
Normal file
|
|
@ -0,0 +1,156 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace Musoka\Zap;
|
||||||
|
|
||||||
|
class DIContainer
|
||||||
|
{
|
||||||
|
protected array $definitions = [];
|
||||||
|
protected array $services = [];
|
||||||
|
protected array $initializedServices = [];
|
||||||
|
|
||||||
|
|
||||||
|
public function get(string $id)
|
||||||
|
{
|
||||||
|
return $this->resolve($id);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function add(string $id, callable $factory): DIContainer
|
||||||
|
{
|
||||||
|
if(isset($this->definitions[$id])) {
|
||||||
|
return $this;
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->definitions[$id] = $factory;
|
||||||
|
return $this;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
public function resolve(string $id)
|
||||||
|
{
|
||||||
|
if (key_exists($id, $this->definitions)) {
|
||||||
|
$factory = $this->definitions[$id];
|
||||||
|
return $factory($this);
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($this->provides($id)) {
|
||||||
|
$this->initialize($id);
|
||||||
|
|
||||||
|
if (!key_exists($id, $this->definitions)) {
|
||||||
|
throw new \Exception(sprintf('Service does not properly provide (%s)', $id));
|
||||||
|
}
|
||||||
|
|
||||||
|
$factory = $this->definitions[$id];
|
||||||
|
return $factory($this);
|
||||||
|
}
|
||||||
|
|
||||||
|
throw new \Exception(sprintf('Definition (%s) has not been added to the container', $id));
|
||||||
|
}
|
||||||
|
|
||||||
|
public function has(string $id): bool
|
||||||
|
{
|
||||||
|
if (key_exists($id, $this->definitions) || $this->provides($id)) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($this->provides($id)) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
private function make(string $class)
|
||||||
|
{
|
||||||
|
try {
|
||||||
|
$reflector = new \ReflectionClass($class);
|
||||||
|
} catch (\ReflectionException $e) {
|
||||||
|
throw new \Exception("Target class [$class] does not exist.", 0, $e);
|
||||||
|
}
|
||||||
|
|
||||||
|
// If the type is not instantiable, such as an Interface or Abstract Class
|
||||||
|
if (! $reflector->isInstantiable()) {
|
||||||
|
throw new \Exception("Target [$class] is not instantiable.");
|
||||||
|
}
|
||||||
|
|
||||||
|
$constructor = $reflector->getConstructor();
|
||||||
|
|
||||||
|
// If there are no constructor, that means there are no dependencies
|
||||||
|
if ($constructor === null) {
|
||||||
|
return new $class;
|
||||||
|
}
|
||||||
|
|
||||||
|
$parameters = $constructor->getParameters();
|
||||||
|
$dependencies = [];
|
||||||
|
|
||||||
|
foreach ($parameters as $parameter) {
|
||||||
|
$type = $parameter->getType();
|
||||||
|
|
||||||
|
if (! $type instanceof \ReflectionNamedType || $type->isBuiltin()) {
|
||||||
|
// Resolve a non-class hinted primitive dependency.
|
||||||
|
if ($parameter->isDefaultValueAvailable()) {
|
||||||
|
$dependencies[] = $parameter->getDefaultValue();
|
||||||
|
} else if ($parameter->isVariadic()) {
|
||||||
|
$dependencies[] = [];
|
||||||
|
} else {
|
||||||
|
throw new \Exception("Unresolvable dependency [$parameter] in class {$parameter->getDeclaringClass()->getName()}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$name = $type->getName();
|
||||||
|
|
||||||
|
// Resolve a class based dependency from the container.
|
||||||
|
try {
|
||||||
|
$dependency = $this->get($name);
|
||||||
|
$dependencies[] = $dependency;
|
||||||
|
} catch (\Exception $e) {
|
||||||
|
if ($parameter->isOptional()) {
|
||||||
|
$dependencies[] = $parameter->getDefaultValue();
|
||||||
|
} else {
|
||||||
|
$dependency = $this->make($parameter->getType()->getName());
|
||||||
|
//$this->register($name, $dependency);
|
||||||
|
$dependencies[] = $dependency;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return $reflector->newInstanceArgs($dependencies);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected function provides(string $id): bool
|
||||||
|
{
|
||||||
|
foreach($this->services as $service) {
|
||||||
|
if($service->provides($id)) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected function initialize(string $id): void
|
||||||
|
{
|
||||||
|
$initialized = false;
|
||||||
|
|
||||||
|
foreach ($this->services as $service) {
|
||||||
|
if (in_array($service->getId(), $this->initializedServices, true)) {
|
||||||
|
$initialized = true;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($service->provides($id)) {
|
||||||
|
$service->initialize();
|
||||||
|
$this->initializedServices[] = $service->getId();
|
||||||
|
$initialized = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!$initialized) {
|
||||||
|
throw new \Exception(
|
||||||
|
sprintf('(%s) is not provided by a service provider', $id)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
79
src/ErrorHandling/Handler.php
Normal file
79
src/ErrorHandling/Handler.php
Normal file
|
|
@ -0,0 +1,79 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace Musoka\Zap\ErrorHandling;
|
||||||
|
|
||||||
|
use Musoka\Zap\Application;
|
||||||
|
|
||||||
|
class Handler
|
||||||
|
{
|
||||||
|
protected bool $debugEnabled;
|
||||||
|
protected array $callbacks = [];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* ErrorHandler constructor.
|
||||||
|
*
|
||||||
|
* Can be used in the future to set up loggers and debugging tools.
|
||||||
|
*/
|
||||||
|
public function __construct(bool $debug = false)
|
||||||
|
{
|
||||||
|
$this->debugEnabled = $debug;
|
||||||
|
|
||||||
|
set_error_handler(function (int $level, string $message, ?string $file, ?int $line, ?array $context = null) {
|
||||||
|
$this->errorHandler($level, $message, $file, $line, $context);
|
||||||
|
});
|
||||||
|
|
||||||
|
set_exception_handler(function (\Throwable $e) {
|
||||||
|
$this->exceptionHandler($e);
|
||||||
|
});
|
||||||
|
|
||||||
|
register_shutdown_function(function () {
|
||||||
|
$this->checkShutdownErrors();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
public function register(callable $callback): self
|
||||||
|
{
|
||||||
|
$this->callbacks[] = $callback;
|
||||||
|
|
||||||
|
return $this;
|
||||||
|
}
|
||||||
|
|
||||||
|
private function errorHandler(int $level, string $message, ?string $file, ?int $line, ?array $context = null)
|
||||||
|
{
|
||||||
|
foreach($this->callbacks as $callback) {
|
||||||
|
if(is_callable($callback)) {
|
||||||
|
($callback)();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if($this->debugEnabled) {
|
||||||
|
$resource = fopen('php://stderr', 'w');
|
||||||
|
fwrite($resource, "An error occurred! {$message}");
|
||||||
|
fwrite($resource, "Occurred in {$file}, at line {$line}.");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private function exceptionHandler(\Throwable $e)
|
||||||
|
{
|
||||||
|
foreach($this->callbacks as $callback) {
|
||||||
|
if(is_callable($callback)) {
|
||||||
|
($callback)($e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if($this->debugEnabled) {
|
||||||
|
$resource = fopen('php://stderr', 'w');
|
||||||
|
fwrite($resource, "An error was thrown! {$e->getMessage()}");
|
||||||
|
fwrite($resource, "{$e->getTraceAsString()}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private function checkShutdownErrors(array $error = [])
|
||||||
|
{
|
||||||
|
$error = $error ?? error_get_last();
|
||||||
|
|
||||||
|
if ($error && $error['type'] === E_ERROR) {
|
||||||
|
$this->errorHandler($error['type'], $error['message'], $error['file'], $error['line']);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
1
src/Helpers/main.php
Normal file
1
src/Helpers/main.php
Normal file
|
|
@ -0,0 +1 @@
|
||||||
|
<?php
|
||||||
50
src/Helpers/paths.php
Normal file
50
src/Helpers/paths.php
Normal file
|
|
@ -0,0 +1,50 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
if(!function_exists('base_path')) {
|
||||||
|
function base_path(string $path = ''): string
|
||||||
|
{
|
||||||
|
return app()->getBasePath($path);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if(!function_exists('config_path')) {
|
||||||
|
function config_path(string $path = ''): string
|
||||||
|
{
|
||||||
|
return app()->getConfigPath($path);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if(!function_exists('lang_path')) {
|
||||||
|
function lang_path(string $path = ''): string
|
||||||
|
{
|
||||||
|
return app()->getLangPath($path);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if(!function_exists('public_path')) {
|
||||||
|
function public_path(string $path = ''): string
|
||||||
|
{
|
||||||
|
return app()->getPublicPath($path);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if(!function_exists('storage_path')) {
|
||||||
|
function storage_path(string $path = ''): string
|
||||||
|
{
|
||||||
|
return app()->getStoragePath($path);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if(!function_exists('resource_path')) {
|
||||||
|
function resource_path(string $path = ''): string
|
||||||
|
{
|
||||||
|
return app()->getResourcePath($path);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if(!function_exists('view_path')) {
|
||||||
|
function view_path(string $path = ''): string
|
||||||
|
{
|
||||||
|
return app()->getViewPath($path);
|
||||||
|
}
|
||||||
|
}
|
||||||
28
src/Helpers/translator.php
Normal file
28
src/Helpers/translator.php
Normal file
|
|
@ -0,0 +1,28 @@
|
||||||
|
<?php
|
||||||
|
if(!function_exists('translator')) {
|
||||||
|
function translator(): \Musoka\Zap\I18n\Translator
|
||||||
|
{
|
||||||
|
return app()->get('translator');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if(!function_exists('translate')) {
|
||||||
|
function translate(string $key, $default = null)
|
||||||
|
{
|
||||||
|
return translator()->getTranslation($key, $default);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if(!function_exists('get_locale')) {
|
||||||
|
function get_locale(): string
|
||||||
|
{
|
||||||
|
return translator()->getLocale();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if(!function_exists('get_fallback_locale')) {
|
||||||
|
function get_fallback_locale(): ?string
|
||||||
|
{
|
||||||
|
return translator()->getFallbackLocale();
|
||||||
|
}
|
||||||
|
}
|
||||||
1
src/Helpers/urls.php
Normal file
1
src/Helpers/urls.php
Normal file
|
|
@ -0,0 +1 @@
|
||||||
|
<?php
|
||||||
12
src/Helpers/views.php
Normal file
12
src/Helpers/views.php
Normal file
|
|
@ -0,0 +1,12 @@
|
||||||
|
<?php
|
||||||
|
if(!function_exists('view')) {
|
||||||
|
function view(string $view, array $data = []): \Musoka\Zap\View\View
|
||||||
|
{
|
||||||
|
$factory = app()->get('view');
|
||||||
|
if($factory) {
|
||||||
|
return $factory->make($view, $data);
|
||||||
|
} else {
|
||||||
|
throw new \Exception('ViewFactory not available as component, cannot use views.');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
10
src/Http/Request.php
Normal file
10
src/Http/Request.php
Normal file
|
|
@ -0,0 +1,10 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace Musoka\Zap\Http;
|
||||||
|
|
||||||
|
use Symfony\Component\HttpFoundation\Request as SymfonyRequest;
|
||||||
|
|
||||||
|
class Request extends SymfonyRequest
|
||||||
|
{
|
||||||
|
|
||||||
|
}
|
||||||
35
src/Http/Response.php
Normal file
35
src/Http/Response.php
Normal file
|
|
@ -0,0 +1,35 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace Musoka\Zap\Http;
|
||||||
|
|
||||||
|
class Response extends \Symfony\Component\HttpFoundation\Response
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Sets the response content.
|
||||||
|
*
|
||||||
|
* If an array or instance of ArrayObject is passed, the
|
||||||
|
* response will be modified to a json response. Otherwise,
|
||||||
|
* the content will be converted to a string.
|
||||||
|
*
|
||||||
|
* @return $this
|
||||||
|
*/
|
||||||
|
public function setContent($content): static
|
||||||
|
{
|
||||||
|
if($content instanceof \ArrayObject || is_array($content)) {
|
||||||
|
$this->headers->set('Content-Type', 'application/json', true);
|
||||||
|
$json = json_encode($content);
|
||||||
|
|
||||||
|
if($json === false) {
|
||||||
|
throw new \InvalidArgumentException(json_last_error_msg());
|
||||||
|
}
|
||||||
|
|
||||||
|
$content = $json;
|
||||||
|
} elseif(!is_string($content)) {
|
||||||
|
$content = (string) $content;
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->content = $content ?? '';
|
||||||
|
|
||||||
|
return $this;
|
||||||
|
}
|
||||||
|
}
|
||||||
36
src/I18n/I18nProvider.php
Normal file
36
src/I18n/I18nProvider.php
Normal file
|
|
@ -0,0 +1,36 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace Musoka\Zap\I18n;
|
||||||
|
|
||||||
|
use Musoka\Zap\Components\AbstractComponentProvider;
|
||||||
|
use Musoka\Zap\Components\ComponentAggregator;
|
||||||
|
|
||||||
|
class I18nProvider extends AbstractComponentProvider
|
||||||
|
{
|
||||||
|
protected array $provides = [
|
||||||
|
'translator'
|
||||||
|
];
|
||||||
|
|
||||||
|
public function registerComponents(ComponentAggregator $aggregator): void
|
||||||
|
{
|
||||||
|
$fallbackLocale = app()->get('config')->get('application.locale.fallback', 'en');
|
||||||
|
$locale = $this->determineLocaleFromUrl();
|
||||||
|
$langDirPath = app()->getLangPath();
|
||||||
|
|
||||||
|
$translator = new Translator($fallbackLocale, $langDirPath);
|
||||||
|
$locale = Translator::checkLocale($locale) ? $locale : $fallbackLocale;
|
||||||
|
$translator->setLocale($locale);
|
||||||
|
|
||||||
|
$aggregator->addComponent('translator', $translator);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected function determineLocaleFromUrl()
|
||||||
|
{
|
||||||
|
$request = app()->get('request');
|
||||||
|
if($request) {
|
||||||
|
return current(explode('/', ltrim($request->getPathInfo(), '/')));
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
164
src/I18n/Translator.php
Normal file
164
src/I18n/Translator.php
Normal file
|
|
@ -0,0 +1,164 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace Musoka\Zap\I18n;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Translator class
|
||||||
|
*
|
||||||
|
* The Translator class provides an implementation for setting the application's
|
||||||
|
* locale and providing translations for use in the application ui. This implementation
|
||||||
|
* uses a regular expression to check if entered locales conform to a standardized format.
|
||||||
|
*/
|
||||||
|
class Translator
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Regular expression used to check the form of locale strings.
|
||||||
|
*/
|
||||||
|
private const LOCALE_REGEX = '/^[A-Za-z]{2,3}([_-][A-Za-z]{4})?([_-][A-Za-z]{2})?$/';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The default application locale.
|
||||||
|
* @var string
|
||||||
|
*/
|
||||||
|
private string $fallbackLocale;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The currently active locale.
|
||||||
|
* @var string
|
||||||
|
*/
|
||||||
|
private string $locale;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The path where translation files are stored.
|
||||||
|
* @var string
|
||||||
|
*/
|
||||||
|
private string $translationsPath;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The currently loaded translations.
|
||||||
|
* @var array
|
||||||
|
*/
|
||||||
|
private array $translations;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Initialize a Translator instance.
|
||||||
|
*
|
||||||
|
* To initialize a Translator, it must know where translation files are to be found and what
|
||||||
|
* locale to set by default on initialization. These values are checked for correctness, upon
|
||||||
|
* failure an exception will be thrown.
|
||||||
|
*
|
||||||
|
* @param string $fallbackLocale
|
||||||
|
* @param string $translationsPath
|
||||||
|
*/
|
||||||
|
public function __construct(string $fallbackLocale, string $translationsPath)
|
||||||
|
{
|
||||||
|
$this->fallbackLocale = $fallbackLocale;
|
||||||
|
$this->locale = $this->fallbackLocale;
|
||||||
|
$this->translationsPath = $translationsPath;
|
||||||
|
|
||||||
|
if(!file_exists($translationsPath) || !is_dir($translationsPath)) {
|
||||||
|
throw new \RuntimeException("Could not initialize translator: invalid translations path.");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public function provides(): string
|
||||||
|
{
|
||||||
|
return 'translator';
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the default locale string.
|
||||||
|
* @return string
|
||||||
|
*/
|
||||||
|
public function getFallbackLocale(): string
|
||||||
|
{
|
||||||
|
return $this->fallbackLocale;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the current locale string.
|
||||||
|
* @return string
|
||||||
|
*/
|
||||||
|
public function getLocale(): string
|
||||||
|
{
|
||||||
|
return $this->locale;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Set the current locale string. If the form is invalid, an exception will be thrown.
|
||||||
|
* @param string $locale
|
||||||
|
* @return $this
|
||||||
|
*/
|
||||||
|
public function setLocale(string $locale): static
|
||||||
|
{
|
||||||
|
if(!static::checkLocale($locale)) {
|
||||||
|
throw new \RuntimeException("Could not set locale: invalid locale format.");
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->locale = $locale;
|
||||||
|
$this->loadTranslations();
|
||||||
|
return $this;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get a translation from the translation array.
|
||||||
|
* @param string $key
|
||||||
|
* @param $default
|
||||||
|
* @return mixed|null|string
|
||||||
|
*/
|
||||||
|
public function getTranslation(string $key, $default = null)
|
||||||
|
{
|
||||||
|
if(empty($this->translations)) {
|
||||||
|
return $default;
|
||||||
|
}
|
||||||
|
|
||||||
|
$keys = explode ('.', $key);
|
||||||
|
|
||||||
|
$value = $this->translations;
|
||||||
|
foreach($keys as $index) {
|
||||||
|
if(!key_exists($index, $value)) return $default;
|
||||||
|
$value = $value[$index];
|
||||||
|
}
|
||||||
|
|
||||||
|
return $value;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if a locale string has the correct format.
|
||||||
|
* @param string $locale
|
||||||
|
* @return bool
|
||||||
|
*/
|
||||||
|
public static function checkLocale(string $locale): bool
|
||||||
|
{
|
||||||
|
return (!empty($locale) && preg_match_all(static::LOCALE_REGEX, $locale));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Load translations for the currently selected locale.
|
||||||
|
* @return void
|
||||||
|
*/
|
||||||
|
private function loadTranslations(): void
|
||||||
|
{
|
||||||
|
$dir = $this->translationsPath;
|
||||||
|
$locale = $this->locale;
|
||||||
|
$localeDir = $dir . DIRECTORY_SEPARATOR . $locale;
|
||||||
|
|
||||||
|
if(!file_exists($localeDir)) {
|
||||||
|
//TODO: is the error really needed?
|
||||||
|
throw new \RuntimeException("Unable to load translations; translation directory not found.");
|
||||||
|
}
|
||||||
|
|
||||||
|
$files = glob($localeDir . DIRECTORY_SEPARATOR . '*.php');
|
||||||
|
if($files === false || empty($files)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->translations = [];
|
||||||
|
foreach($files as $file) {
|
||||||
|
$translations = require $file;
|
||||||
|
$key = basename($file, '.php');
|
||||||
|
$this->translations[$key] = $translations;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
69
src/Routing/IsRepository.php
Normal file
69
src/Routing/IsRepository.php
Normal file
|
|
@ -0,0 +1,69 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace Musoka\Zap\Routing;
|
||||||
|
|
||||||
|
trait IsRepository
|
||||||
|
{
|
||||||
|
protected $basePattern = '';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @var array The array with registered routes.
|
||||||
|
*/
|
||||||
|
protected array $routes;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Register a new route.
|
||||||
|
*
|
||||||
|
* @param string $pattern The uri pattern to match
|
||||||
|
* @param array|string $methods One or more HTTP methods to match
|
||||||
|
* @param string|array|callable $callback A callback to run if this route is called
|
||||||
|
* @param array $middlewares List of extra middlewares to run for this route
|
||||||
|
* @return Route
|
||||||
|
*/
|
||||||
|
public function register(string $pattern, array|string $methods, string|array|callable $callback, array $middlewares = []): Route
|
||||||
|
{
|
||||||
|
$pattern = '/' . trim($this->basePattern . $pattern, '/');
|
||||||
|
$route = new Route($pattern, $methods, $callback, $middlewares);
|
||||||
|
|
||||||
|
foreach($route->getMethods() as $method) {
|
||||||
|
$this->routes[$method][] = $route;
|
||||||
|
}
|
||||||
|
|
||||||
|
return $route;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function get(string $pattern, string|array|callable $callback, array $middlewares = []): Route
|
||||||
|
{
|
||||||
|
return $this->register($pattern, 'GET', $callback, $middlewares);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function post(string $pattern, string|array|callable $callback, array $middlewares = []): Route
|
||||||
|
{
|
||||||
|
return $this->register($pattern, 'POST', $callback, $middlewares);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function put(string $pattern, string|array|callable $callback, array $middlewares = []): Route
|
||||||
|
{
|
||||||
|
return $this->register($pattern, 'PUT', $callback, $middlewares);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function delete(string $pattern, string|array|callable $callback, array $middlewares = []): Route
|
||||||
|
{
|
||||||
|
return $this->register($pattern, 'DELETE', $callback, $middlewares);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function options(string $pattern, string|array|callable $callback, array $middlewares = []): Route
|
||||||
|
{
|
||||||
|
return $this->register($pattern, 'OPTIONS', $callback, $middlewares);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function patch(string $pattern, string|array|callable $callback, array $middlewares = []): Route
|
||||||
|
{
|
||||||
|
return $this->register($pattern, 'PATCH', $callback, $middlewares);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function head(string $pattern, string|array|callable $callback, array $middlewares = []): Route
|
||||||
|
{
|
||||||
|
return $this->register($pattern, 'HEAD', $callback, $middlewares);
|
||||||
|
}
|
||||||
|
}
|
||||||
26
src/Routing/LocalizedRepository.php
Normal file
26
src/Routing/LocalizedRepository.php
Normal file
|
|
@ -0,0 +1,26 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace Musoka\Zap\Routing;
|
||||||
|
|
||||||
|
class LocalizedRepository extends Repository
|
||||||
|
{
|
||||||
|
|
||||||
|
|
||||||
|
public function __construct()
|
||||||
|
{
|
||||||
|
parent::__construct();
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
public function register(string $pattern, array|string $methods, string|array|callable $callback, array $middlewares = []): Route
|
||||||
|
{
|
||||||
|
$pattern = '/' . trim($pattern, '/');
|
||||||
|
$route = new LocalizedRoute($pattern, $methods, $callback, $middlewares);
|
||||||
|
|
||||||
|
foreach($route->getMethods() as $method) {
|
||||||
|
$this->routes[$method][] = $route;
|
||||||
|
}
|
||||||
|
|
||||||
|
return $route;
|
||||||
|
}
|
||||||
|
}
|
||||||
21
src/Routing/LocalizedRoute.php
Normal file
21
src/Routing/LocalizedRoute.php
Normal file
|
|
@ -0,0 +1,21 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace Musoka\Zap\Routing;
|
||||||
|
|
||||||
|
class LocalizedRoute extends Route
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* @var string[]
|
||||||
|
*/
|
||||||
|
protected $translatedPatterns;
|
||||||
|
|
||||||
|
public function __construct(string $pattern, array|string $methods, callable|array|string $callback, array $supportedLocales = [], array $middlewares = [])
|
||||||
|
{
|
||||||
|
parent::__construct($pattern, $methods, $callback, $middlewares);
|
||||||
|
|
||||||
|
|
||||||
|
foreach($supportedLocales as $locale) {
|
||||||
|
$this->translatedPatterns[$locale] = $pattern;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
113
src/Routing/Repository.php
Normal file
113
src/Routing/Repository.php
Normal file
|
|
@ -0,0 +1,113 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace Musoka\Zap\Routing;
|
||||||
|
|
||||||
|
class Repository
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* @var array The array with registered routes.
|
||||||
|
*/
|
||||||
|
protected array $routes;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* RoutingRepository constructor.
|
||||||
|
*/
|
||||||
|
public function __construct()
|
||||||
|
{
|
||||||
|
$this->routes = [];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create a group of localized routes.
|
||||||
|
*
|
||||||
|
* Group a number of routes as localized routes. The callback function will
|
||||||
|
* get an instance of LocalizedRepository passed to it, which will expose
|
||||||
|
* extra functions for localization support.
|
||||||
|
*
|
||||||
|
* @param callable $callback
|
||||||
|
* @return void
|
||||||
|
*/
|
||||||
|
public function localized(callable $callback): void
|
||||||
|
{
|
||||||
|
$localizedGroup = new LocalizedRepository();
|
||||||
|
call_user_func($callback, $localizedGroup);
|
||||||
|
|
||||||
|
//Add localized routes to list
|
||||||
|
$this->routes = array_merge_recursive($this->routes, $localizedGroup->collectRoutes());
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Register a new route.
|
||||||
|
*
|
||||||
|
* @param string $pattern The uri pattern to match
|
||||||
|
* @param array|string $methods One or more HTTP methods to match
|
||||||
|
* @param string|array|callable $callback A callback to run if this route is called
|
||||||
|
* @param array $middlewares List of extra middlewares to run for this route
|
||||||
|
* @return Route
|
||||||
|
*/
|
||||||
|
public function register(string $pattern, array|string $methods, string|array|callable $callback, array $middlewares = []): Route
|
||||||
|
{
|
||||||
|
$pattern = '/' . trim($pattern, '/');
|
||||||
|
$route = new Route($pattern, $methods, $callback, $middlewares);
|
||||||
|
|
||||||
|
foreach($route->getMethods() as $method) {
|
||||||
|
$this->routes[$method][] = $route;
|
||||||
|
}
|
||||||
|
|
||||||
|
return $route;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function get(string $pattern, string|array|callable $callback, array $middlewares = []): Route
|
||||||
|
{
|
||||||
|
return $this->register($pattern, 'GET', $callback, $middlewares);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function post(string $pattern, string|array|callable $callback, array $middlewares = []): Route
|
||||||
|
{
|
||||||
|
return $this->register($pattern, 'POST', $callback, $middlewares);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function put(string $pattern, string|array|callable $callback, array $middlewares = []): Route
|
||||||
|
{
|
||||||
|
return $this->register($pattern, 'PUT', $callback, $middlewares);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function delete(string $pattern, string|array|callable $callback, array $middlewares = []): Route
|
||||||
|
{
|
||||||
|
return $this->register($pattern, 'DELETE', $callback, $middlewares);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function options(string $pattern, string|array|callable $callback, array $middlewares = []): Route
|
||||||
|
{
|
||||||
|
return $this->register($pattern, 'OPTIONS', $callback, $middlewares);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function patch(string $pattern, string|array|callable $callback, array $middlewares = []): Route
|
||||||
|
{
|
||||||
|
return $this->register($pattern, 'PATCH', $callback, $middlewares);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function head(string $pattern, string|array|callable $callback, array $middlewares = []): Route
|
||||||
|
{
|
||||||
|
return $this->register($pattern, 'HEAD', $callback, $middlewares);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the list of registered routes.
|
||||||
|
*
|
||||||
|
* @return array
|
||||||
|
*/
|
||||||
|
public function collectRoutes(): array
|
||||||
|
{
|
||||||
|
//Loop through groups and register their routes
|
||||||
|
/*foreach($this->groups as $group) {
|
||||||
|
$group(); //Invoke group to run its callback
|
||||||
|
$groupRoutes = $group->getRoutes();
|
||||||
|
|
||||||
|
//Merge into main routes list.
|
||||||
|
$this->routes = array_merge_recursive($groupRoutes, $this->routes);
|
||||||
|
}*/
|
||||||
|
|
||||||
|
return $this->routes;
|
||||||
|
}
|
||||||
|
}
|
||||||
260
src/Routing/Route.php
Normal file
260
src/Routing/Route.php
Normal file
|
|
@ -0,0 +1,260 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace Musoka\Zap\Routing;
|
||||||
|
|
||||||
|
class Route
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* @var string Name for the route. Multple routes can have the same name, linking them.
|
||||||
|
*/
|
||||||
|
private string $name;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @var string Pattern for this route.
|
||||||
|
*/
|
||||||
|
private string $pattern;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @var string|array|callable The callback to run when executing the route.
|
||||||
|
*/
|
||||||
|
private $callback;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @var array<string> The HTTP verbs applicable to this route.
|
||||||
|
*/
|
||||||
|
private array $methods = [];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @var array<string> A list of middlewares that should be run before executing this route.
|
||||||
|
*/
|
||||||
|
private array $middlewares = [];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @var array<string> List of variables taken from the pattern. These are used to pass parameters to the callback function.
|
||||||
|
*/
|
||||||
|
private array $vars = [];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Route constructor.
|
||||||
|
*
|
||||||
|
* The constructor will initialize a Route object with a pattern, list of applicable HTTP methods and the callback
|
||||||
|
* to run for this route. Optionally a list of middlewares to run can also be passed (but can also be set later).
|
||||||
|
*/
|
||||||
|
public function __construct(string $pattern, string|array $methods, string|array|callable $callback, array $middlewares = [])
|
||||||
|
{
|
||||||
|
if ($methods === []) {
|
||||||
|
throw new \InvalidArgumentException('HTTP methods argument was empty; must contain at least one method');
|
||||||
|
}
|
||||||
|
|
||||||
|
if(is_string($methods)) $methods = [$methods];
|
||||||
|
foreach($methods as $key => $method) {
|
||||||
|
$methods[$key] = strtoupper($method);
|
||||||
|
if (!in_array($methods[$key], ['GET', 'POST', 'PUT', 'DELETE', 'OPTIONS', 'PATCH', 'HEAD'])) {
|
||||||
|
throw new \InvalidArgumentException("Invalid method '{$method}' specified.");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->pattern = $pattern;
|
||||||
|
$this->callback = $callback;
|
||||||
|
$this->methods = $methods;
|
||||||
|
$this->middlewares = $middlewares;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the name for the route.
|
||||||
|
*
|
||||||
|
* @return string
|
||||||
|
*/
|
||||||
|
public function getName(): string
|
||||||
|
{
|
||||||
|
return $this->name;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Set the name for the route.
|
||||||
|
*
|
||||||
|
* @param string $name
|
||||||
|
* @return $this
|
||||||
|
*/
|
||||||
|
public function setName(string $name): static
|
||||||
|
{
|
||||||
|
$this->name = $name;
|
||||||
|
return $this;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the url pattern for the route.
|
||||||
|
*
|
||||||
|
* @return string
|
||||||
|
*/
|
||||||
|
public function getPattern(): string
|
||||||
|
{
|
||||||
|
return $this->pattern;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function setPattern(string $pattern): static
|
||||||
|
{
|
||||||
|
$this->pattern = $pattern;
|
||||||
|
return $this;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the callback function for this route.
|
||||||
|
*
|
||||||
|
* @return string|array|callable
|
||||||
|
*/
|
||||||
|
public function getCallback(): string|array|callable
|
||||||
|
{
|
||||||
|
return $this->callback;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Set the callback function for this route.
|
||||||
|
*
|
||||||
|
* @param string|array|callable $callback
|
||||||
|
* @return $this
|
||||||
|
*/
|
||||||
|
public function setCallback(string|array|callable $callback): static
|
||||||
|
{
|
||||||
|
$this->callback = $callback;
|
||||||
|
return $this;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the HTTP methods applicable to this route.
|
||||||
|
*
|
||||||
|
* @return string[]
|
||||||
|
*/
|
||||||
|
public function getMethods(): array
|
||||||
|
{
|
||||||
|
return $this->methods;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Set the HTTP methods applicable to this route.
|
||||||
|
*
|
||||||
|
* @param string|string[] $methods
|
||||||
|
* @return $this
|
||||||
|
*/
|
||||||
|
public function setMethods(string|array $methods): static
|
||||||
|
{
|
||||||
|
if(is_string($methods)) {
|
||||||
|
$methods = [$methods];
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->methods = $methods;
|
||||||
|
return $this;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the list of middlewares to be run for this route.
|
||||||
|
*
|
||||||
|
* @return string[]
|
||||||
|
*/
|
||||||
|
public function getMiddlewares(): array
|
||||||
|
{
|
||||||
|
return $this->middlewares;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function setMiddlewares(array $middlewares): static
|
||||||
|
{
|
||||||
|
$this->middlewares = $middlewares;
|
||||||
|
return $this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function addMiddleware(string|array $middleware): static
|
||||||
|
{
|
||||||
|
if(is_string($middleware)) {
|
||||||
|
$middleware = [$middleware];
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach($middleware as $value) {
|
||||||
|
if(!in_array($value, $this->middlewares)) {
|
||||||
|
$this->middlewares[] = $value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return $this;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return array
|
||||||
|
*/
|
||||||
|
public function getVarsNames(): array
|
||||||
|
{
|
||||||
|
preg_match_all('/{[^}]*}/', $this->pattern, $matches);
|
||||||
|
return reset($matches) ?? [];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return bool
|
||||||
|
*/
|
||||||
|
public function hasVars(): bool
|
||||||
|
{
|
||||||
|
return $this->getVarsNames() !== [];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return string[]
|
||||||
|
*/
|
||||||
|
public function getVars(): array
|
||||||
|
{
|
||||||
|
return $this->vars;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param string $path
|
||||||
|
* @param string $method
|
||||||
|
* @return bool
|
||||||
|
*/
|
||||||
|
public function match(string $path, string $method): bool
|
||||||
|
{
|
||||||
|
$regex = $this->getPattern();
|
||||||
|
foreach ($this->getVarsNames() as $variable) {
|
||||||
|
$varName = trim($variable, '{\}');
|
||||||
|
$regex = str_replace($variable, '(?P<' . $varName . '>[^/]++)', $regex);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (in_array($method, $this->getMethods()) && preg_match('#^' . $regex . '$#sD', self::trimPath($path), $matches)) {
|
||||||
|
$values = array_filter($matches, static function ($key) {
|
||||||
|
return is_string($key);
|
||||||
|
}, ARRAY_FILTER_USE_KEY);
|
||||||
|
foreach ($values as $key => $value) {
|
||||||
|
$this->vars[$key] = $value;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return mixed
|
||||||
|
*/
|
||||||
|
public function run()
|
||||||
|
{
|
||||||
|
//TODO: middlewares
|
||||||
|
//TODO: allow for automatically injecting dependencies for function arguments
|
||||||
|
|
||||||
|
if(is_callable($this->callback)) {
|
||||||
|
return call_user_func_array($this->callback, $this->vars);
|
||||||
|
} elseif (is_array($this->callback) && [0,1] == array_keys($this->callback)) {
|
||||||
|
$class = $this->callback[0];
|
||||||
|
$method = $this->callback[1];
|
||||||
|
if(class_exists($class) && method_exists($class, $method)) {
|
||||||
|
$controller = new $class();
|
||||||
|
return $controller->$method(...$this->vars);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
throw new \RuntimeException("Could not resolve callable function for route.");
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param string $path
|
||||||
|
* @return string
|
||||||
|
*/
|
||||||
|
protected static function trimPath(string $path): string
|
||||||
|
{
|
||||||
|
return '/' . rtrim(ltrim(trim($path), '/'), '/');
|
||||||
|
}
|
||||||
|
}
|
||||||
40
src/Routing/RouteGroup.php
Normal file
40
src/Routing/RouteGroup.php
Normal file
|
|
@ -0,0 +1,40 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace Musoka\Zap\Routing;
|
||||||
|
|
||||||
|
class RouteGroup
|
||||||
|
{
|
||||||
|
use IsRepository;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @var callable Callback to call to actually register the routes.
|
||||||
|
*/
|
||||||
|
protected $callback;
|
||||||
|
|
||||||
|
public function __construct(string $pattern, callable $callback)
|
||||||
|
{
|
||||||
|
$this->basePattern = $pattern; //Group pattern is used as base pattern for all routes in group.
|
||||||
|
$this->callback = $callback;
|
||||||
|
$this->routes = [];
|
||||||
|
}
|
||||||
|
|
||||||
|
public function __invoke(): void
|
||||||
|
{
|
||||||
|
($this->callback)($this);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getPattern(): string
|
||||||
|
{
|
||||||
|
return $this->basePattern;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getCallback(): callable
|
||||||
|
{
|
||||||
|
return $this->callback;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getRoutes(): array
|
||||||
|
{
|
||||||
|
return $this->routes;
|
||||||
|
}
|
||||||
|
}
|
||||||
113
src/Routing/RouteRepository.php
Normal file
113
src/Routing/RouteRepository.php
Normal file
|
|
@ -0,0 +1,113 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace Musoka\Zap\Routing;
|
||||||
|
|
||||||
|
class RouteRepository
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* @var array The array with registered routes.
|
||||||
|
*/
|
||||||
|
protected array $routes;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* RoutingRepository constructor.
|
||||||
|
*/
|
||||||
|
public function __construct()
|
||||||
|
{
|
||||||
|
$this->routes = [];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create a group of localized routes.
|
||||||
|
*
|
||||||
|
* Group a number of routes as localized routes. The callback function will
|
||||||
|
* get an instance of LocalizedRepository passed to it, which will expose
|
||||||
|
* extra functions for localization support.
|
||||||
|
*
|
||||||
|
* @param callable $callback
|
||||||
|
* @return void
|
||||||
|
*/
|
||||||
|
/*public function localized(callable $callback): void
|
||||||
|
{
|
||||||
|
$localizedGroup = new LocalizedRepository();
|
||||||
|
call_user_func($callback, $localizedGroup);
|
||||||
|
|
||||||
|
//Add localized routes to list
|
||||||
|
$this->routes = array_merge_recursive($this->routes, $localizedGroup->collectRoutes());
|
||||||
|
}*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Register a new route.
|
||||||
|
*
|
||||||
|
* @param string $pattern The uri pattern to match
|
||||||
|
* @param array|string $methods One or more HTTP methods to match
|
||||||
|
* @param string|array|callable $callback A callback to run if this route is called
|
||||||
|
* @param array $middlewares List of extra middlewares to run for this route
|
||||||
|
* @return Route
|
||||||
|
*/
|
||||||
|
public function register(string $pattern, array|string $methods, string|array|callable $callback, array $middlewares = []): Route
|
||||||
|
{
|
||||||
|
$pattern = '/' . trim($pattern, '/');
|
||||||
|
$route = new Route($pattern, $methods, $callback, $middlewares);
|
||||||
|
|
||||||
|
foreach($route->getMethods() as $method) {
|
||||||
|
$this->routes[$method][] = $route;
|
||||||
|
}
|
||||||
|
|
||||||
|
return $route;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function get(string $pattern, string|array|callable $callback, array $middlewares = []): Route
|
||||||
|
{
|
||||||
|
return $this->register($pattern, 'GET', $callback, $middlewares);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function post(string $pattern, string|array|callable $callback, array $middlewares = []): Route
|
||||||
|
{
|
||||||
|
return $this->register($pattern, 'POST', $callback, $middlewares);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function put(string $pattern, string|array|callable $callback, array $middlewares = []): Route
|
||||||
|
{
|
||||||
|
return $this->register($pattern, 'PUT', $callback, $middlewares);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function delete(string $pattern, string|array|callable $callback, array $middlewares = []): Route
|
||||||
|
{
|
||||||
|
return $this->register($pattern, 'DELETE', $callback, $middlewares);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function options(string $pattern, string|array|callable $callback, array $middlewares = []): Route
|
||||||
|
{
|
||||||
|
return $this->register($pattern, 'OPTIONS', $callback, $middlewares);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function patch(string $pattern, string|array|callable $callback, array $middlewares = []): Route
|
||||||
|
{
|
||||||
|
return $this->register($pattern, 'PATCH', $callback, $middlewares);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function head(string $pattern, string|array|callable $callback, array $middlewares = []): Route
|
||||||
|
{
|
||||||
|
return $this->register($pattern, 'HEAD', $callback, $middlewares);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the list of registered routes.
|
||||||
|
*
|
||||||
|
* @return array
|
||||||
|
*/
|
||||||
|
public function collectRoutes(): array
|
||||||
|
{
|
||||||
|
//Loop through groups and register their routes
|
||||||
|
/*foreach($this->groups as $group) {
|
||||||
|
$group(); //Invoke group to run its callback
|
||||||
|
$groupRoutes = $group->getRoutes();
|
||||||
|
|
||||||
|
//Merge into main routes list.
|
||||||
|
$this->routes = array_merge_recursive($groupRoutes, $this->routes);
|
||||||
|
}*/
|
||||||
|
|
||||||
|
return $this->routes;
|
||||||
|
}
|
||||||
|
}
|
||||||
179
src/Routing/Router.php
Normal file
179
src/Routing/Router.php
Normal file
|
|
@ -0,0 +1,179 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace Musoka\Zap\Routing;
|
||||||
|
|
||||||
|
use Musoka\Zap\Application;
|
||||||
|
use Symfony\Component\HttpFoundation\HeaderBag;
|
||||||
|
use Musoka\Zap\Http\Request;
|
||||||
|
|
||||||
|
class Router implements RouterInterface
|
||||||
|
{
|
||||||
|
private Request $request;
|
||||||
|
|
||||||
|
protected RouteRepository $repository;
|
||||||
|
|
||||||
|
private ?Route $currentRoute;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @var array
|
||||||
|
*/
|
||||||
|
private array $routes;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @var string
|
||||||
|
*/
|
||||||
|
private string $requestUri;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @var string
|
||||||
|
*/
|
||||||
|
private string $requestMethod;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @var array
|
||||||
|
*/
|
||||||
|
private HeaderBag $requestHeaders;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param Application $app
|
||||||
|
*/
|
||||||
|
public function __construct(Request $request, string $routeDirPath, string $cacheDirPath)
|
||||||
|
{
|
||||||
|
$this->request = $request;
|
||||||
|
$this->currentRoute = null;
|
||||||
|
|
||||||
|
//Load routes from cache, or fall back to loading from file
|
||||||
|
if(!$this->loadFromCache($cacheDirPath)) {
|
||||||
|
$this->loadFromRouteFile($routeDirPath);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
protected function loadFromCache(string $cachePath): bool
|
||||||
|
{
|
||||||
|
if(!file_exists($cachePath) || !file_exists($cachePath . DIRECTORY_SEPARATOR . 'route-cache.php')) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->routes = require $cachePath . DIRECTORY_SEPARATOR . 'route-cache.php';
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected function loadFromRouteFile(string $routePath): void
|
||||||
|
{
|
||||||
|
if(!file_exists($routePath) || !file_exists($routePath . DIRECTORY_SEPARATOR . 'routes.php')) {
|
||||||
|
throw new \RuntimeException('Unable to initialize routes: route file not found.');
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->repository = new RouteRepository();
|
||||||
|
$routes = require $routePath . DIRECTORY_SEPARATOR . 'routes.php';
|
||||||
|
|
||||||
|
if(!is_callable($routes)) {
|
||||||
|
throw new \RuntimeException('Unable to initialize routes: route file does not contain callable function.');
|
||||||
|
}
|
||||||
|
|
||||||
|
//Initialize routes
|
||||||
|
call_user_func($routes, $this->repository);
|
||||||
|
$this->routes = $this->repository->collectRoutes();
|
||||||
|
}
|
||||||
|
|
||||||
|
protected function readRequestData()
|
||||||
|
{
|
||||||
|
$uri = $this->request->getPathInfo();
|
||||||
|
|
||||||
|
// Remove trailing slash + enforce a slash at the start
|
||||||
|
$this->requestUri = '/' . trim($uri, '/');
|
||||||
|
$this->requestMethod = $this->request->getMethod();
|
||||||
|
$this->requestHeaders = $this->request->headers;
|
||||||
|
|
||||||
|
// If it's a HEAD request override it to being GET and prevent any output, as per HTTP Specification
|
||||||
|
// @url http://www.w3.org/Protocols/rfc2616/rfc2616-sec9.html#sec9.4
|
||||||
|
/*if ($_SERVER['REQUEST_METHOD'] == 'HEAD') {
|
||||||
|
ob_start();
|
||||||
|
$method = 'GET';
|
||||||
|
}*/ //TODO: should this be done in application?
|
||||||
|
|
||||||
|
/*$headers = [];*/
|
||||||
|
|
||||||
|
// If getallheaders() is available, use that
|
||||||
|
/*if (function_exists('getallheaders')) {
|
||||||
|
$headers = getallheaders();
|
||||||
|
|
||||||
|
// getallheaders() can return false if something went wrong
|
||||||
|
if ($headers !== false) {
|
||||||
|
$this->requestHeaders = $headers;
|
||||||
|
return $headers;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Method getallheaders() not available or went wrong: manually extract them
|
||||||
|
foreach ($_SERVER as $name => $value) {
|
||||||
|
if ((substr($name, 0, 5) == 'HTTP_') || ($name == 'CONTENT_TYPE') || ($name == 'CONTENT_LENGTH')) {
|
||||||
|
$headers[str_replace([' ', 'Http'], ['-', 'HTTP'], ucwords(strtolower(str_replace('_', ' ', substr($name, 5)))))] = $value;
|
||||||
|
}
|
||||||
|
}*/
|
||||||
|
}
|
||||||
|
|
||||||
|
protected function findRoute(string $path, string $method): ?Route
|
||||||
|
{
|
||||||
|
$method = strtoupper($method);
|
||||||
|
|
||||||
|
if(key_exists($method, $this->routes)) {
|
||||||
|
foreach($this->routes[$method] as $route) {
|
||||||
|
if($route->match($path, $method)) {
|
||||||
|
return $route;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected function handleRoute(Route $route)
|
||||||
|
{
|
||||||
|
//Todo: run route middleware and route handler.
|
||||||
|
//Maybe make middleware part of a base controller class?
|
||||||
|
|
||||||
|
return $route->run();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return mixed|bool Returns a mixed response, or a boolean false if there was no valid route.
|
||||||
|
*/
|
||||||
|
public function run()
|
||||||
|
{
|
||||||
|
//Parse request
|
||||||
|
$this->readRequestData();
|
||||||
|
|
||||||
|
//Find the first matching route. Only one route will ever be executed.
|
||||||
|
$route = $this->findRoute($this->requestUri, $this->requestMethod);
|
||||||
|
|
||||||
|
//If a route is found, handle it and return its response.
|
||||||
|
//Otherwise, return a boolean false to denote no route could be found.
|
||||||
|
if($route) {
|
||||||
|
$this->currentRoute = $route;
|
||||||
|
return $this->handleRoute($route);
|
||||||
|
} else {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
//If the request was a HEAD request, empty the output buffer.
|
||||||
|
/*if ($_SERVER['REQUEST_METHOD'] == 'HEAD') {
|
||||||
|
ob_end_clean();
|
||||||
|
}*/ //TODO: should this be done in application?
|
||||||
|
}
|
||||||
|
|
||||||
|
public function buildCache()
|
||||||
|
{
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getCurrentRoute(): ?Route
|
||||||
|
{
|
||||||
|
return $this->currentRoute;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getRoute(string $pattern, ?string $method): ?Route
|
||||||
|
{
|
||||||
|
return $this->findRoute($pattern, $method ?? 'GET');
|
||||||
|
}
|
||||||
|
}
|
||||||
8
src/Routing/RouterInterface.php
Normal file
8
src/Routing/RouterInterface.php
Normal file
|
|
@ -0,0 +1,8 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace Musoka\Zap\Routing;
|
||||||
|
|
||||||
|
interface RouterInterface
|
||||||
|
{
|
||||||
|
public function run();
|
||||||
|
}
|
||||||
28
src/Routing/RouterProvider.php
Normal file
28
src/Routing/RouterProvider.php
Normal file
|
|
@ -0,0 +1,28 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace Musoka\Zap\Routing;
|
||||||
|
|
||||||
|
use Musoka\Zap\Components\AbstractComponentProvider;
|
||||||
|
use Musoka\Zap\Components\ComponentAggregator;
|
||||||
|
|
||||||
|
class RouterProvider extends AbstractComponentProvider
|
||||||
|
{
|
||||||
|
protected array $provides = [
|
||||||
|
'router',
|
||||||
|
'url'
|
||||||
|
];
|
||||||
|
|
||||||
|
public function registerComponents(ComponentAggregator $aggregator): void
|
||||||
|
{
|
||||||
|
$request = $aggregator->get('request');
|
||||||
|
$routeDirPath = realpath(app()->getBasePath('routes'));
|
||||||
|
$cacheDirPath = realpath(app()->getCachePath());
|
||||||
|
|
||||||
|
$router = new Router($request, $routeDirPath, $cacheDirPath);
|
||||||
|
|
||||||
|
//TODO: set up URL generator
|
||||||
|
|
||||||
|
$aggregator->addComponent('router', $router);
|
||||||
|
$aggregator->addComponent('url', function () { return null; });
|
||||||
|
}
|
||||||
|
}
|
||||||
71
src/Routing/UrlGenerator.php
Normal file
71
src/Routing/UrlGenerator.php
Normal file
|
|
@ -0,0 +1,71 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace Musoka\Zap\Routing;
|
||||||
|
|
||||||
|
use Musoka\Zap\I18n\Translator;
|
||||||
|
use Symfony\Component\HttpFoundation\Request;
|
||||||
|
|
||||||
|
class UrlGenerator
|
||||||
|
{
|
||||||
|
protected $translator;
|
||||||
|
|
||||||
|
protected $routes;
|
||||||
|
|
||||||
|
protected $request;
|
||||||
|
|
||||||
|
protected $baseUrl;
|
||||||
|
|
||||||
|
public function __construct(string $baseUrl, Request $request, Translator $translator)
|
||||||
|
{
|
||||||
|
$this->request = $request;
|
||||||
|
$this->translator = $translator;
|
||||||
|
$this->baseUrl = $baseUrl ?? $request->getBaseUrl();
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getBaseUrl(string $path = '', array $data = [], ?string $locale = null): string
|
||||||
|
{
|
||||||
|
$overrideLocale = $locale ?? $this->translator->getLocale();
|
||||||
|
$localePrefix = ($overrideLocale != $this->translator->getDefaultLocale())
|
||||||
|
? $overrideLocale . DIRECTORY_SEPARATOR
|
||||||
|
: '';
|
||||||
|
|
||||||
|
$url = $this->baseUrl . $localePrefix . ($path ? DIRECTORY_SEPARATOR.$path : '');
|
||||||
|
|
||||||
|
//Attach data as a query string.
|
||||||
|
if(!empty($data)) {
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
return $url;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getUrl(string $path = '', array $data = [], ?string $locale = null): string
|
||||||
|
{
|
||||||
|
return $this->getBaseUrl($path, $data, $locale);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getStorageUrl(string $path = '', array $data = [], ?string $locale = null): string
|
||||||
|
{
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getAssetUrl(string $path = '', array $data = [], ?string $locale = null): string
|
||||||
|
{
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getRouteUrl(string $route = '', array $data = [], ?string $locale = null): string
|
||||||
|
{
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
|
||||||
|
public function setRoutes(array $routes): bool
|
||||||
|
{
|
||||||
|
if(!empty($this->routes)) {
|
||||||
|
$this->routes = $routes;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
18
src/View/Renderable.php
Normal file
18
src/View/Renderable.php
Normal file
|
|
@ -0,0 +1,18 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace Musoka\Zap\View;
|
||||||
|
|
||||||
|
interface Renderable
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Convert the object state into a string representation.
|
||||||
|
* @return string
|
||||||
|
*/
|
||||||
|
public function __toString(): string;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Render the object as a string.
|
||||||
|
* @return string
|
||||||
|
*/
|
||||||
|
public function render(): string;
|
||||||
|
}
|
||||||
186
src/View/View.php
Normal file
186
src/View/View.php
Normal file
|
|
@ -0,0 +1,186 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace Musoka\Zap\View;
|
||||||
|
|
||||||
|
enum SectionMode {
|
||||||
|
case OVERWRITE;
|
||||||
|
case PREPEND;
|
||||||
|
case APPEND;
|
||||||
|
}
|
||||||
|
|
||||||
|
class View implements Renderable
|
||||||
|
{
|
||||||
|
protected ViewFactory $factory;
|
||||||
|
protected string $view;
|
||||||
|
protected array $data;
|
||||||
|
|
||||||
|
protected array $sections = [];
|
||||||
|
protected ?string $sectionName;
|
||||||
|
protected ?SectionMode $sectionMode;
|
||||||
|
|
||||||
|
protected ?string $extendView;
|
||||||
|
protected ?array $extendData;
|
||||||
|
|
||||||
|
|
||||||
|
public function __construct(ViewFactory $factory, string $view, array $data = [])
|
||||||
|
{
|
||||||
|
$this->factory = $factory;
|
||||||
|
$this->view = $view;
|
||||||
|
$this->data = $data;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Set data that should be made available to the view.
|
||||||
|
*
|
||||||
|
* Consecutive calls will append data, overwriting existing
|
||||||
|
* data if keys were already set.
|
||||||
|
*
|
||||||
|
* @param array|null $data
|
||||||
|
* @return array|void
|
||||||
|
*/
|
||||||
|
public function data(array $data = null)
|
||||||
|
{
|
||||||
|
if (is_null($data)) {
|
||||||
|
return $this->data;
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->data = array_merge($this->data, $data);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Render the view object to a string.
|
||||||
|
*
|
||||||
|
* @return string
|
||||||
|
* @throws \Throwable
|
||||||
|
*/
|
||||||
|
public function __toString(): string
|
||||||
|
{
|
||||||
|
return $this->render();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Render the view to a string.
|
||||||
|
*
|
||||||
|
* @param array $data
|
||||||
|
* @return string
|
||||||
|
* @throws \Throwable
|
||||||
|
*/
|
||||||
|
public function render(array $data = []): string
|
||||||
|
{
|
||||||
|
$this->data($data);
|
||||||
|
$viewPath = $this->factory->resolveViewPath($this->view);
|
||||||
|
|
||||||
|
try {
|
||||||
|
$level = ob_get_level();
|
||||||
|
ob_start();
|
||||||
|
|
||||||
|
(function() {
|
||||||
|
extract($this->data);
|
||||||
|
include func_get_arg(0);
|
||||||
|
})($viewPath);
|
||||||
|
|
||||||
|
$content = ob_get_clean();
|
||||||
|
|
||||||
|
if (isset($this->extendView)) {
|
||||||
|
$extend = $this->factory->make($this->extendView);
|
||||||
|
$extend->sections = array_merge($this->sections, ['content' => $content]);
|
||||||
|
$content = $extend->render($this->extendData);
|
||||||
|
}
|
||||||
|
|
||||||
|
return $content;
|
||||||
|
}
|
||||||
|
catch(\Throwable $e) {
|
||||||
|
while (ob_get_level() > $level) {
|
||||||
|
ob_end_clean();
|
||||||
|
}
|
||||||
|
|
||||||
|
throw $e;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public function extends(string $view, array $data = []): void
|
||||||
|
{
|
||||||
|
$this->extendView = $view;
|
||||||
|
$this->extendData = $data;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function insert(string $view, array $data = []): string
|
||||||
|
{
|
||||||
|
return $this->factory->make($view, $data)->render();
|
||||||
|
}
|
||||||
|
|
||||||
|
public function section(string $name, string $default = ''): string
|
||||||
|
{
|
||||||
|
return $this->sections[$name] ?? $default;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function startSection(string $name, SectionMode $mode = SectionMode::OVERWRITE): void
|
||||||
|
{
|
||||||
|
$this->sectionMode = $mode;
|
||||||
|
|
||||||
|
if ($name === 'content') {
|
||||||
|
throw new \LogicException(
|
||||||
|
'The section name "content" is reserved.'
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isset($this->sectionName) && !empty($this->sectionName)) {
|
||||||
|
throw new \LogicException('You cannot nest sections within other sections.');
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->sectionName = $name;
|
||||||
|
|
||||||
|
ob_start();
|
||||||
|
}
|
||||||
|
|
||||||
|
public function appendSection(string $name): void
|
||||||
|
{
|
||||||
|
$this->startSection($name, SectionMode::APPEND);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function prependSection(string $name): void
|
||||||
|
{
|
||||||
|
$this->startSection($name, SectionMode::PREPEND);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function endSection(): void
|
||||||
|
{
|
||||||
|
if (is_null($this->sectionName)) {
|
||||||
|
throw new \LogicException(
|
||||||
|
'You must start a section before you can stop it.'
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!isset($this->sections[$this->sectionName])) {
|
||||||
|
$this->sections[$this->sectionName] = '';
|
||||||
|
}
|
||||||
|
|
||||||
|
switch ($this->sectionMode) {
|
||||||
|
|
||||||
|
case SectionMode::OVERWRITE:
|
||||||
|
$this->sections[$this->sectionName] = ob_get_clean();
|
||||||
|
break;
|
||||||
|
|
||||||
|
case SectionMode::APPEND:
|
||||||
|
$this->sections[$this->sectionName] .= ob_get_clean();
|
||||||
|
break;
|
||||||
|
|
||||||
|
case SectionMode::PREPEND:
|
||||||
|
$this->sections[$this->sectionName] = ob_get_clean().$this->sections[$this->sectionName];
|
||||||
|
break;
|
||||||
|
|
||||||
|
}
|
||||||
|
$this->sectionName = null;
|
||||||
|
$this->sectionMode = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function escape(string $string): string
|
||||||
|
{
|
||||||
|
return htmlspecialchars($string);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function e(string $string): string
|
||||||
|
{
|
||||||
|
return $this->escape($string);
|
||||||
|
}
|
||||||
|
}
|
||||||
58
src/View/ViewFactory.php
Normal file
58
src/View/ViewFactory.php
Normal file
|
|
@ -0,0 +1,58 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace Musoka\Zap\View;
|
||||||
|
|
||||||
|
class ViewFactory
|
||||||
|
{
|
||||||
|
protected string $viewDir;
|
||||||
|
|
||||||
|
public function __construct(string $viewDir)
|
||||||
|
{
|
||||||
|
$this->viewDir = $viewDir;
|
||||||
|
if(!file_exists($this->viewDir)) {
|
||||||
|
throw new \RuntimeException('View directory must be a valid existing directory.');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public function resolveViewPath(string $view): string
|
||||||
|
{
|
||||||
|
$viewPath = $this->viewDir . DIRECTORY_SEPARATOR . ltrim($view, '/');
|
||||||
|
|
||||||
|
if(is_dir($viewPath)) {
|
||||||
|
$indexPath = rtrim($viewPath, '/') . DIRECTORY_SEPARATOR . 'index.php';
|
||||||
|
if(file_exists($indexPath)) return $indexPath;
|
||||||
|
} else {
|
||||||
|
if(file_exists($viewPath)) return $viewPath;
|
||||||
|
elseif(file_exists($viewPath . '.php')) return $viewPath . '.php';
|
||||||
|
}
|
||||||
|
|
||||||
|
throw new \RuntimeException("View {$viewPath} could not be found.");
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if a template exists.
|
||||||
|
* @param string $view
|
||||||
|
* @return boolean
|
||||||
|
*/
|
||||||
|
public function exists(string $view): bool
|
||||||
|
{
|
||||||
|
try {
|
||||||
|
$this->resolveViewPath($view);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
catch (\RuntimeException $e) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create a new template.
|
||||||
|
* @param string $view
|
||||||
|
* @param array $data
|
||||||
|
* @return View
|
||||||
|
*/
|
||||||
|
public function make(string $view, array $data = []): View
|
||||||
|
{
|
||||||
|
return new View($this, $view, $data);
|
||||||
|
}
|
||||||
|
}
|
||||||
21
src/View/ViewProvider.php
Normal file
21
src/View/ViewProvider.php
Normal file
|
|
@ -0,0 +1,21 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace Musoka\Zap\View;
|
||||||
|
|
||||||
|
use Musoka\Zap\Components\AbstractComponentProvider;
|
||||||
|
use Musoka\Zap\Components\ComponentAggregator;
|
||||||
|
|
||||||
|
class ViewProvider extends AbstractComponentProvider
|
||||||
|
{
|
||||||
|
protected array $provides = [
|
||||||
|
'view'
|
||||||
|
];
|
||||||
|
|
||||||
|
public function registerComponents(ComponentAggregator $aggregator): void
|
||||||
|
{
|
||||||
|
$viewDirPath = app()->getViewPath();
|
||||||
|
$viewFactory = new ViewFactory($viewDirPath);
|
||||||
|
|
||||||
|
$aggregator->addComponent('view', $viewFactory);
|
||||||
|
}
|
||||||
|
}
|
||||||
12
src/helpers.php
Normal file
12
src/helpers.php
Normal file
|
|
@ -0,0 +1,12 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
if(!function_exists('app')) {
|
||||||
|
function app(): ?\Musoka\Zap\Application
|
||||||
|
{
|
||||||
|
return \Musoka\Zap\Application::getInstance();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
require_once __DIR__ . DIRECTORY_SEPARATOR . 'Helpers' . DIRECTORY_SEPARATOR . 'paths.php';
|
||||||
|
require_once __DIR__ . DIRECTORY_SEPARATOR . 'Helpers' . DIRECTORY_SEPARATOR . 'views.php';
|
||||||
|
require_once __DIR__ . DIRECTORY_SEPARATOR . 'Helpers' . DIRECTORY_SEPARATOR . 'translator.php';
|
||||||
Loading…
Reference in a new issue