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