Initialize Nest core library.

This commit is contained in:
Madeorsk 2024-11-08 12:12:38 +01:00
commit 22296b8d7c
Signed by: Madeorsk
GPG key ID: 677E51CA765BB79F
17 changed files with 693 additions and 0 deletions

6
.gitignore vendored Normal file
View file

@ -0,0 +1,6 @@
# IDEA
.idea/
*.iml
# Composer
vendor/

26
composer.json Normal file
View file

@ -0,0 +1,26 @@
{
"version": "1.0",
"name": "nest/core",
"description": "Nest framework core.",
"type": "library",
"authors": [
{
"name": "Madeorsk",
"email": "madeorsk@protonmail.com"
}
],
"autoload": {
"psr-4": {
"Nest\\": "src/"
},
"files": [
"src/Utils/Array.php",
"src/Utils/Paths.php",
"src/Utils/Reflection.php",
"src/Utils/String.php"
]
},
"require": {
"php": "^8.3"
}
}

20
composer.lock generated Normal file
View file

@ -0,0 +1,20 @@
{
"_readme": [
"This file locks the dependencies of your project to a known state",
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
"This file is @generated automatically"
],
"content-hash": "f86ab8aef4bacd58e94fb6ceae48e892",
"packages": [],
"packages-dev": [],
"aliases": [],
"minimum-stability": "stable",
"stability-flags": {},
"prefer-stable": false,
"prefer-lowest": false,
"platform": {
"php": "^8.3"
},
"platform-dev": {},
"plugin-api-version": "2.6.0"
}

209
src/Application.php Normal file
View file

@ -0,0 +1,209 @@
<?php
namespace Nest;
use Nest\Exceptions\Services\Configuration\InvalidServiceConfigurationDefinitionException;
use Nest\Exceptions\Services\Configuration\UndefinedServiceConfigurationException;
use Nest\Services\ServiceConfiguration;
use function Nest\Utils\get_class_name_from_fullname;
/**
* Main nest application class.
*/
abstract class Application
{
/**
* Main instance of the application.
* @var static
*/
private static Application $application;
/**
* Register the created application.
* @param static $application The application.
* @return static The application.
*/
public static function register(Application $application): static
{
self::$application = $application;
return $application;
}
/**
* Get the main instance of the application.
* @return static The application.
*/
public static function get(): static
{
return self::$application;
}
/**
* Paths builder.
* @var Paths
*/
private Paths $paths;
/**
* Services configurations associative array.
* @var array<string, ServiceConfiguration>
*/
private array $servicesConfigurations;
/**
* Initialize a new application.
* @param string $basePath Base path of the application.
* @param string|null $globalContextIdentifier Global context identifier. This is useful if you need different configurations and initializations depending on call context.
* @throws UndefinedServiceConfigurationException
*/
public function __construct(string $basePath, private readonly ?string $globalContextIdentifier = null)
{
$this->paths = $this->initializePaths($basePath);
$this->configure();
$this->initializeServices();
$this->initialize();
}
/**
* Get the global context identifier. This is useful if you need different configurations and initializations depending on call context.
* @return string|null The global context identifier.
*/
public function getGlobalContextIdentifier(): ?string
{
return $this->globalContextIdentifier;
}
/**
* Initialize application paths.
* @param string $basePath Base path of the application.
* @return Paths Paths builder.
*/
protected function initializePaths(string $basePath): Paths
{
// Create default paths builder.
return new Paths($basePath);
}
/**
* Application paths.
* @return Paths
*/
public function paths(): Paths
{
return $this->paths;
}
/**
* Initialize application services.
* @return void
* @throws UndefinedServiceConfigurationException
*/
private function initializeServices(): void
{
foreach (class_uses($this) as $usedTrait)
{
// Build the service initializer name.
$serviceInitializationMethod = "__nest__".get_class_name_from_fullname($usedTrait);
if (method_exists($this, $serviceInitializationMethod))
// If there is a service initializer, we run it.
$this->$serviceInitializationMethod();
}
}
/**
* Set a service configuration for a given base class.
* @param string $serviceConfigurationBaseClass The service configuration base class.
* @param ServiceConfiguration $serviceConfiguration Initialized service configuration instance.
* @return void
* @throws InvalidServiceConfigurationDefinitionException
*/
protected function setServiceConfiguration(string $serviceConfigurationBaseClass, ServiceConfiguration $serviceConfiguration): void
{
if (!is_a($serviceConfiguration, $serviceConfigurationBaseClass))
throw new InvalidServiceConfigurationDefinitionException($serviceConfigurationBaseClass, get_class($serviceConfiguration));
$this->servicesConfigurations[$serviceConfigurationBaseClass] = $serviceConfiguration;
}
/**
* Get a service configuration by its base class.
* @template T of ServiceConfiguration
* @param class-string<T> $serviceConfigurationClass Base class of the service configuration.
* @return T The service configuration instance.
* @throws UndefinedServiceConfigurationException
*/
protected function getServiceConfiguration(string $serviceConfigurationClass): ServiceConfiguration
{
if (empty($this->servicesConfigurations[$serviceConfigurationClass]))
throw new UndefinedServiceConfigurationException($serviceConfigurationClass);
/**
* Get service configuration.
* @var T $config
*/
$config = $this->servicesConfigurations[$serviceConfigurationClass];
return $config;
}
/**
* Setup application configuration.
* Started before services initialization and application initialization.
* @return void
*/
protected abstract function configure(): void;
/**
* Initialize application.
* Started after services initialization.
* @return void
*/
public function initialize(): void
{}
/**
* Exception handler instance.
* @var ExceptionHandler
*/
private ExceptionHandler $exceptionHandler;
/**
* Get a new exception handler instance.
* @return ExceptionHandler New exception handler instance.
*/
protected function newExceptionHandler(): ExceptionHandler
{
// Return default exception handler.
return new ExceptionHandler();
}
/**
* Get exception handler instance for this application.
* @return ExceptionHandler Exception handler instance.
*/
public function exceptionHandler(): ExceptionHandler
{
if (empty($this->exceptionHandler))
// No exception handler, initializing one.
$this->exceptionHandler = $this->newExceptionHandler();
// Return initialized exception handler.
return $this->exceptionHandler;
}
/**
* Start an engine in the application.
* @param callable $engine Engine instance or callable.
* @return void
*/
public function startEngine(callable $engine): void
{
// Set application exception handler while executing the engine.
set_exception_handler($this->exceptionHandler());
$engine();
restore_exception_handler();
}
}

41
src/ExceptionHandler.php Normal file
View file

@ -0,0 +1,41 @@
<?php
namespace Nest;
use Throwable;
/**
* Default exception handler for Nest application.
* Support multiple sub-handlers, typically added by engines.
*/
class ExceptionHandler
{
/**
* @var (callable(Throwable): void)[]
*/
protected array $handlers = [];
/**
* Run all exception handlers.
* @param Throwable $exception The exception.
* @return void
*/
public function __invoke(Throwable $exception): void
{
foreach ($this->handlers as $handler)
{ // Execute each registered exception handler.
$handler($exception);
}
}
/**
* Add an exception handler.
* @param callable(Throwable): void $handler The exception handler function.
* @return $this
*/
public function add(callable $handler): static
{
$this->handlers[] = $handler;
return $this;
}
}

View file

@ -0,0 +1,22 @@
<?php
namespace Nest\Exceptions;
use function Nest\Utils\get_class_name_from_fullname;
use function Nest\Utils\str_remove_suffix;
/**
* Generic class of a Nest exception.
*/
class Exception extends \Exception
{
/**
* Get generic error ID from class name.
* @return string Error ID.
*/
public function getErrorId(): string
{
// Get the current exception class name and remove "Exception" suffix.
return str_remove_suffix(get_class_name_from_fullname(get_class($this)), "Exception");
}
}

View file

@ -0,0 +1,22 @@
<?php
namespace Nest\Exceptions;
use Throwable;
/**
* Thrown when an invalid class or object is passed to a function.
*/
class InvalidTypeException extends Exception
{
/**
* @param string $expectedType The expected type.
* @param string $actualType The actual class or object type.
* @param int $code [optional] The Exception code.
* @param null|Throwable $previous [optional] The previous throwable used for the exception chaining.
*/
public function __construct(public string $expectedType, public string $actualType, int $code = 0, ?Throwable $previous = null)
{
parent::__construct("Expected type $this->expectedType, got $this->actualType.", $code, $previous);
}
}

View file

@ -0,0 +1,23 @@
<?php
namespace Nest\Exceptions\Services\Configuration;
use Nest\Exceptions\Services\ServiceException;
use Throwable;
/**
* Exception thrown when an invalid service configuration is defined for a service.
*/
class InvalidServiceConfigurationDefinitionException extends ServiceException
{
/**
* @param string $serviceConfigurationBaseClass Service configuration base class.
* @param string $givenServiceConfigurationClass Given service configuration class.
* @param int $code [optional] The Exception code.
* @param null|Throwable $previous [optional] The previous throwable used for the exception chaining.
*/
public function __construct(public string $serviceConfigurationBaseClass, public string $givenServiceConfigurationClass, int $code = 0, ?Throwable $previous = null)
{
parent::__construct("Invalid service configuration, expected {$this->serviceConfigurationBaseClass}, {$this->givenServiceConfigurationClass} given.", $code, $previous);
}
}

View file

@ -0,0 +1,22 @@
<?php
namespace Nest\Exceptions\Services\Configuration;
use Nest\Exceptions\Services\ServiceException;
use Throwable;
/**
* Exception thrown when a requested service configuration is not defined.
*/
class UndefinedServiceConfigurationException extends ServiceException
{
/**
* @param string $serviceConfigurationClass Requested service configuration.
* @param int $code [optional] The Exception code.
* @param null|Throwable $previous [optional] The previous throwable used for the exception chaining.
*/
public function __construct(public string $serviceConfigurationClass, int $code = 0, ?Throwable $previous = null)
{
parent::__construct("Undefined service configuration: {$this->serviceConfigurationClass}", $code, $previous);
}
}

View file

@ -0,0 +1,9 @@
<?php
namespace Nest\Exceptions\Services;
use Nest\Exceptions\Exception;
class ServiceException extends Exception
{
}

View file

@ -0,0 +1,22 @@
<?php
namespace Nest\Exceptions;
use Throwable;
/**
* Exception thrown when an instance method does not exists.
*/
class UnknownInstanceMethodException extends Exception
{
/**
* @param string $className Class where method is not found.
* @param string $methodName Method not found.
* @param int $code [optional] The Exception code.
* @param null|Throwable $previous [optional] The previous throwable used for the exception chaining.
*/
public function __construct(public string $className, public string $methodName, int $code = 0, ?Throwable $previous = null)
{
parent::__construct("Cannot find $this->methodName in $this->className.", $code, $previous);
}
}

43
src/Paths.php Normal file
View file

@ -0,0 +1,43 @@
<?php
namespace Nest;
/**
* Application paths builder.
*/
class Paths
{
/**
* Create a new paths manager from its main path.
* @param string $appPath The main path of the application.
*/
public function __construct(protected string $appPath)
{}
/**
* Get main path of the application.
* @return string
*/
public function getAppPath(): string
{
return $this->appPath;
}
/**
* Get temporary files path.
* @return string
*/
public function getTempPath(): string
{
return "{$this->getAppPath()}/tmp";
}
/**
* Get configuration files path.
* @return string
*/
public function getConfigPath(): string
{
return "{$this->getAppPath()}/config";
}
}

View file

@ -0,0 +1,7 @@
<?php
namespace Nest\Services;
abstract class ServiceConfiguration
{
}

78
src/Utils/Array.php Normal file
View file

@ -0,0 +1,78 @@
<?php
namespace Nest\Utils;
/**
* Get first element of an array.
* @template Element
* @param Element[] $arr The array on which to get the first element.
* @return Element|null First element of the array. NULL if array is empty.
*/
function array_first(array $arr): mixed
{
// Try to get first element key.
if (!is_null($firstKey = array_key_first($arr)))
// There is a first element, returning it.
return $arr[$firstKey];
else
// No first element, returning null.
return null;
}
/**
* Get first element of an array if it's one, or the direct value.
* @template Element
* @param Element|Element[] $arr_or_val Array or value.
* @return Element Value or array first value.
*/
function array_first_or_val(mixed $arr_or_val): mixed
{
return is_array($arr_or_val) ? array_first($arr_or_val) : $arr_or_val;
}
/**
* Flatten arrays in one array.
* @template Element
* @param Element[] $items Array of arrays to flatten.
* @return Element[] Flattened array.
*/
function array_flatten(array $items): array
{
return array_reduce($items, function ($carry, $item) {
return array_merge($carry, is_array($item) ? array_flatten($item) : [$item]);
}, []);
}
/**
* Deduplicate values in an array, but faster than array_unique.
* Warning: duplicated values keys may be lost.
*
* @template Element
* @param Element[] $array Array to deduplicate.
* @return Element[] Deduplicated array.
*/
function array_unique_quick(array $array): array
{
return array_flip(array_flip($array));
}
/**
* Tests whether at least one element in the array passes the test implemented by the provided function.
* It doesn't modify the array.
* @template Element
* @param callable $test_function - The function that implements the test.
* @param Element[] $array - The array to test.
* @return bool - True if it finds an element of the array for which the provided function returns true; returns false otherwise.
*/
function array_some(callable $test_function, array $array): bool
{
foreach ($array as $key => $value)
{ // For each (key => value) association, tests if the test function returns true.
if ($test_function($value, $key))
// If it returns true, we already can return true as we only search for one element that pass.
return true;
}
return false; // We found no (key => value) association that passed the test, returning false.
}

13
src/Utils/Paths.php Normal file
View file

@ -0,0 +1,13 @@
<?php
namespace Nest\Utils;
/**
* Join path parts using directory separator.
* @param string ...$pathParts Path parts to join.
* @return string A single joined path.
*/
function path_join(string ...$pathParts): string
{
return implode(DIRECTORY_SEPARATOR, $pathParts);
}

48
src/Utils/Reflection.php Normal file
View file

@ -0,0 +1,48 @@
<?php
namespace Nest\Utils;
use Nest\Exceptions\InvalidTypeException;
/**
* Extract the class name from its full name (\Namespace\Classname).
* @param string $class_fullname Full name of the class, with its namespace.
* @return string The class name.
*/
function get_class_name_from_fullname(string $class_fullname): string
{ // Keep last part of the string, after the last '\'.
return substr($class_fullname, mb_strrpos($class_fullname, "\\") + 1);
}
/**
* Check that the given object or class is of the expected type.
* @template T
* @param string|object $object_or_class Object or class to check.
* @param class-string<T> $expected_type Expected class type.
* @return true
* @throws InvalidTypeException
*/
function expect_type(mixed $object_or_class, string $expected_type): true
{
if (!is_a($object_or_class, $expected_type))
throw new InvalidTypeException($expected_type, $object_or_class);
return true;
}
/**
* Get all classes of an object or a class.
* @param string|object $object_or_class
* @return string[] All classes of an object or a class (full inheritance tree).
*/
function get_all_classes(mixed $object_or_class): array
{
// Get the class of the current object.
if (!is_string($object_or_class))
$object_or_class = get_class($object_or_class);
// Try to get the parent class of the current class.
$parentClass = get_parent_class($object_or_class);
// Merge classes list, preprending with the current class.
return [$object_or_class, ...($parentClass ? get_all_classes($parentClass) : [])];
}

82
src/Utils/String.php Normal file
View file

@ -0,0 +1,82 @@
<?php
namespace Nest\Utils;
/**
* Convert a camelCase string in a snake_case string.
* @param string $camel_cased_str The camelCase string to convert.
* @return string The converted string in snake_case.
*/
function str_camel_to_snake(string $camel_cased_str): string
{
return ltrim(strtolower(preg_replace("/[A-Z]([A-Z](?![a-z]))*/", "_$0", $camel_cased_str)), "_");
}
/**
* Get the plural form of every word in the snake_cased string.
* @param string $snake_name_to_pluralize The snake_cased string to pluralize.
* @return string The pluralized snake_cased string.
*/
function str_snake_pluralize(string $snake_name_to_pluralize): string
{
// Break the string to an array containing each word.
$parts = explode("_", $snake_name_to_pluralize);
// Add an 's' to each word.
$parts = array_map(fn (string $part) => "{$part}s", $parts);
// Glue all words together with '_'.
return implode("_", $parts);
}
/**
* Get the singular form of every word in the snake_cased string.
* @param string $snake_name_to_singularize The snake_cased string to singularize.
* @return string The singularized snake_cased string.
*/
function str_snake_singularize(string $snake_name_to_singularize): string
{
// Break the string to an array containing each word.
$parts = explode("_", $snake_name_to_singularize);
// Remove an 's' from each word, if there is one.
$parts = array_map(function (string $part) {
$sPos = strlen($part) - 1;
return $part[$sPos] == 's' ? substr($part, 0, $sPos) : $part;
}, $parts);
// Glue all words together with '_'.
return implode("_", $parts);
}
/**
* Remove prefixes from a base string, if present.
* @param string $base Base string.
* @param string[] $prefix Prefix to remove, if present.
* @return string
*/
function str_remove_prefix(string $base, string ...$prefix): string
{
foreach($prefix as $_prefix)
{ // Remove each passed prefix.
if (str_starts_with($base, $_prefix))
// Prefix is present, removing it.
$base = substr($base, strlen($_prefix));
}
return $base; // Return altered base.
}
/**
* Remove suffixes from a base string, if present.
* @param string $base Base string.
* @param string[] $suffix Suffix to remove, if present.
* @return string
*/
function str_remove_suffix(string $base, string ...$suffix): string
{
foreach($suffix as $_suffix)
{ // Remove each passed suffix.
if (str_ends_with($base, $_suffix))
// Suffix is present, removing it.
$base = substr($base, 0, strlen($base) - strlen($_suffix));
}
return $base; // Return altered base.
}