First Commit
This commit is contained in:
commit
e1ad5f5089
5
.gitignore
vendored
Normal file
5
.gitignore
vendored
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
.cache/
|
||||||
|
.idea/
|
||||||
|
build/
|
||||||
|
vendor/
|
||||||
|
composer.lock
|
||||||
61
composer.json
Normal file
61
composer.json
Normal file
@ -0,0 +1,61 @@
|
|||||||
|
{
|
||||||
|
"name": "phpnative/framework",
|
||||||
|
"type": "library",
|
||||||
|
"license": "MIT",
|
||||||
|
"autoload": {
|
||||||
|
"psr-4": {
|
||||||
|
"PHPNative\\Framework\\": "src/PHPNative/Framework/src",
|
||||||
|
"PHPNative\\UI\\": "src/PHPNative/UI/src",
|
||||||
|
"PHPNative\\Tailwind\\": "src/PHPNative/Tailwind/src",
|
||||||
|
"PHPNative\\Core\\": "src/PHPNative/Core/src",
|
||||||
|
"PHPNative\\Container\\": "src/PHPNative/Container/src"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"replace": {
|
||||||
|
"phpnative/tailwind": "self.version"
|
||||||
|
},
|
||||||
|
"autoload-dev": {
|
||||||
|
"psr-4": {
|
||||||
|
"PHPNative\\Tailwind\\Tests\\": "src/PHPNative/Tailwind/tests",
|
||||||
|
"PHPNative\\Core\\Tests\\": "src/PHPNative/Core/tests",
|
||||||
|
"PHPNative\\UI\\Tests\\": "src/PHPNative/UI/tests",
|
||||||
|
"PHPNative\\Container\\Tests\\": "src/PHPNative/Container/tests",
|
||||||
|
"PHPNative\\Framework\\Tests\\": "src/PHPNative/Framework/tests"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"authors": [
|
||||||
|
{
|
||||||
|
"name": "Thomas Peterson",
|
||||||
|
"email": "info@thomas-peterson.de"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"require": {
|
||||||
|
"ext-sdl": "*",
|
||||||
|
"ext-parallel": "*"
|
||||||
|
},
|
||||||
|
"require-dev": {
|
||||||
|
"friendsofphp/php-cs-fixer": "^3.21",
|
||||||
|
"phpat/phpat": "^0.10.14",
|
||||||
|
"phpstan/phpstan": "^1.10.0",
|
||||||
|
"phpunit/phpunit": "^10.2",
|
||||||
|
"rector/rector": "^1.2",
|
||||||
|
"spaze/phpstan-disallowed-calls": "^3.1",
|
||||||
|
"symplify/monorepo-builder": "^11.2"
|
||||||
|
},
|
||||||
|
"scripts": {
|
||||||
|
"phpunit": "vendor/bin/phpunit --display-warnings --display-skipped --display-deprecations --display-errors --display-notices",
|
||||||
|
"coverage": "vendor/bin/phpunit --coverage-html build/reports/html --coverage-clover build/reports/clover.xml",
|
||||||
|
"csfixer": "vendor/bin/php-cs-fixer fix --allow-risky=yes",
|
||||||
|
"phpstan": "vendor/bin/phpstan analyse src tests --memory-limit=1G",
|
||||||
|
"rector": "vendor/bin/rector process --no-ansi",
|
||||||
|
"merge": "vendor/bin/monorepo-builder merge",
|
||||||
|
"qa": [
|
||||||
|
"composer merge",
|
||||||
|
"./tempest discovery:clear",
|
||||||
|
"vendor/bin/rector process",
|
||||||
|
"composer csfixer",
|
||||||
|
"composer phpunit",
|
||||||
|
"composer phpstan"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
9
monorepo-builder.php
Normal file
9
monorepo-builder.php
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
use Symplify\MonorepoBuilder\Config\MBConfig;
|
||||||
|
|
||||||
|
return static function (MBConfig $mbConfig): void {
|
||||||
|
$mbConfig->packageDirectories([__DIR__ . '/src/PHPNative']);
|
||||||
|
};
|
||||||
17
src/PHPNative/Container/composer.json
Normal file
17
src/PHPNative/Container/composer.json
Normal file
@ -0,0 +1,17 @@
|
|||||||
|
{
|
||||||
|
"name": "phpnative/container",
|
||||||
|
"license": "MIT",
|
||||||
|
"require": {
|
||||||
|
"php": "^8.3"
|
||||||
|
},
|
||||||
|
"autoload": {
|
||||||
|
"psr-4": {
|
||||||
|
"PHPNative\\Container\\": "src"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"autoload-dev": {
|
||||||
|
"psr-4": {
|
||||||
|
"PHPNative\\Container\\Tests\\": "tests"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
33
src/PHPNative/Container/src/Container.php
Normal file
33
src/PHPNative/Container/src/Container.php
Normal file
@ -0,0 +1,33 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace PHPNative\Container\src;
|
||||||
|
|
||||||
|
use ReflectionClass;
|
||||||
|
|
||||||
|
interface Container
|
||||||
|
{
|
||||||
|
public function register(string $className, callable $definition): self;
|
||||||
|
|
||||||
|
public function singleton(string $className, callable $definition): self;
|
||||||
|
|
||||||
|
public function config(object $config): self;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @template TClassName
|
||||||
|
* @param class-string<TClassName> $className
|
||||||
|
* @return TClassName
|
||||||
|
*/
|
||||||
|
public function get(string $className, mixed ...$params): object;
|
||||||
|
|
||||||
|
public function call(object $object, string $methodName, mixed ...$params): mixed;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @template T of \PHPNative\Container\src\Initializer
|
||||||
|
* @template U of \PHPNative\Container\src\DynamicInitializer
|
||||||
|
* @param ReflectionClass|class-string<T>|class-string<U> $initializerClass
|
||||||
|
* @return self
|
||||||
|
*/
|
||||||
|
public function addInitializer(ReflectionClass|string $initializerClass): self;
|
||||||
|
}
|
||||||
25
src/PHPNative/Container/src/ContainerLog.php
Normal file
25
src/PHPNative/Container/src/ContainerLog.php
Normal file
@ -0,0 +1,25 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace PHPNative\Container\src;
|
||||||
|
|
||||||
|
interface ContainerLog
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* @return \PHPNative\Container\src\Context[]
|
||||||
|
*/
|
||||||
|
public function getStack(): array;
|
||||||
|
|
||||||
|
public function startResolving(): self;
|
||||||
|
|
||||||
|
public function addContext(Context $context): self;
|
||||||
|
|
||||||
|
public function addDependency(Dependency $dependency): self;
|
||||||
|
|
||||||
|
public function currentContext(): Context;
|
||||||
|
|
||||||
|
public function currentDependency(): ?Dependency;
|
||||||
|
|
||||||
|
public function getOrigin(): string;
|
||||||
|
}
|
||||||
84
src/PHPNative/Container/src/Context.php
Normal file
84
src/PHPNative/Container/src/Context.php
Normal file
@ -0,0 +1,84 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace PHPNative\Container\src;
|
||||||
|
|
||||||
|
use ReflectionClass;
|
||||||
|
use ReflectionFunction;
|
||||||
|
use ReflectionMethod;
|
||||||
|
|
||||||
|
final class Context
|
||||||
|
{
|
||||||
|
public function __construct(
|
||||||
|
private ReflectionClass|ReflectionMethod|ReflectionFunction $reflector,
|
||||||
|
/** @var \PHPNative\Container\src\Dependency[] $dependencies */
|
||||||
|
private array $dependencies = [],
|
||||||
|
) {
|
||||||
|
}
|
||||||
|
|
||||||
|
public function addDependency(Dependency $dependency): self
|
||||||
|
{
|
||||||
|
$this->dependencies[] = $dependency;
|
||||||
|
|
||||||
|
return $this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function currentDependency(): ?Dependency
|
||||||
|
{
|
||||||
|
return $this->dependencies[array_key_last($this->dependencies)] ?? null;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getName(): string
|
||||||
|
{
|
||||||
|
return match($this->reflector::class) {
|
||||||
|
ReflectionClass::class => $this->reflector->getName(),
|
||||||
|
ReflectionMethod::class => $this->reflector->getDeclaringClass()->getName(),
|
||||||
|
ReflectionFunction::class => $this->reflector->getName() . ' in ' . $this->reflector->getFileName() . ':' . $this->reflector->getStartLine(),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getShortName(): string
|
||||||
|
{
|
||||||
|
return match($this->reflector::class) {
|
||||||
|
ReflectionClass::class => $this->reflector->getShortName(),
|
||||||
|
ReflectionMethod::class => $this->reflector->getDeclaringClass()->getShortName(),
|
||||||
|
ReflectionFunction::class => $this->reflector->getShortName() . ' in ' . $this->reflector->getFileName() . ':' . $this->reflector->getStartLine(),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
public function __toString(): string
|
||||||
|
{
|
||||||
|
return match($this->reflector::class) {
|
||||||
|
ReflectionClass::class => $this->classToString(),
|
||||||
|
ReflectionMethod::class => $this->methodToString(),
|
||||||
|
ReflectionFunction::class => $this->functionToString(),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
private function classToString(): string
|
||||||
|
{
|
||||||
|
return $this->reflector->getShortName();
|
||||||
|
}
|
||||||
|
|
||||||
|
private function methodToString(): string
|
||||||
|
{
|
||||||
|
return $this->reflector->getDeclaringClass()->getShortName() . '::' . $this->reflector->getName() . '(' . $this->dependenciesToString() . ')';
|
||||||
|
}
|
||||||
|
|
||||||
|
private function functionToString(): string
|
||||||
|
{
|
||||||
|
return $this->reflector->getShortName() . ' in ' . $this->reflector->getFileName() . ':' . $this->reflector->getStartLine() . '(' . $this->dependenciesToString() . ')';
|
||||||
|
}
|
||||||
|
|
||||||
|
private function dependenciesToString(): string
|
||||||
|
{
|
||||||
|
return implode(
|
||||||
|
', ',
|
||||||
|
array_map(
|
||||||
|
fn (Dependency $dependency) => (string) $dependency,
|
||||||
|
$this->dependencies,
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
87
src/PHPNative/Container/src/Dependency.php
Normal file
87
src/PHPNative/Container/src/Dependency.php
Normal file
@ -0,0 +1,87 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace PHPNative\Container\src;
|
||||||
|
|
||||||
|
use ReflectionClass;
|
||||||
|
use ReflectionIntersectionType;
|
||||||
|
use ReflectionNamedType;
|
||||||
|
use ReflectionParameter;
|
||||||
|
use ReflectionType;
|
||||||
|
use ReflectionUnionType;
|
||||||
|
|
||||||
|
final readonly class Dependency
|
||||||
|
{
|
||||||
|
public function __construct(
|
||||||
|
public ReflectionParameter|ReflectionClass $reflector,
|
||||||
|
) {
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getId(): string
|
||||||
|
{
|
||||||
|
return $this->typeToString($this->getType());
|
||||||
|
}
|
||||||
|
|
||||||
|
public function __toString(): string
|
||||||
|
{
|
||||||
|
$typeToString = $this->typeToString($this->getType());
|
||||||
|
$parts = explode('\\', $typeToString);
|
||||||
|
$typeToString = $parts[array_key_last($parts)];
|
||||||
|
|
||||||
|
return implode(
|
||||||
|
' ',
|
||||||
|
array_filter([
|
||||||
|
$typeToString,
|
||||||
|
'$' . $this->reflector->getName(),
|
||||||
|
]),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
private function getType(): string|ReflectionType
|
||||||
|
{
|
||||||
|
return match($this->reflector::class) {
|
||||||
|
ReflectionParameter::class => $this->reflector->getType(),
|
||||||
|
ReflectionClass::class => $this->reflector->getName(),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
private function typeToString(string|ReflectionType|null $type): ?string
|
||||||
|
{
|
||||||
|
if ($type === null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (is_string($type)) {
|
||||||
|
return $type;
|
||||||
|
}
|
||||||
|
|
||||||
|
return match($type::class) {
|
||||||
|
ReflectionIntersectionType::class => $this->intersectionTypeToString($type),
|
||||||
|
ReflectionNamedType::class => $type->getName(),
|
||||||
|
ReflectionUnionType::class => $this->unionTypeToString($type),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
private function intersectionTypeToString(ReflectionIntersectionType $type): string
|
||||||
|
{
|
||||||
|
return implode(
|
||||||
|
'&',
|
||||||
|
array_map(
|
||||||
|
fn (ReflectionType $subType) => $this->typeToString($subType),
|
||||||
|
$type->getTypes(),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
private function unionTypeToString(ReflectionUnionType $type): string
|
||||||
|
{
|
||||||
|
return implode(
|
||||||
|
'|',
|
||||||
|
array_map(
|
||||||
|
fn (ReflectionType $subType) => $this->typeToString($subType),
|
||||||
|
$type->getTypes(),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
12
src/PHPNative/Container/src/DynamicInitializer.php
Normal file
12
src/PHPNative/Container/src/DynamicInitializer.php
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace PHPNative\Container\src;
|
||||||
|
|
||||||
|
interface DynamicInitializer
|
||||||
|
{
|
||||||
|
public function canInitialize(string $className): bool;
|
||||||
|
|
||||||
|
public function initialize(string $className, Container $container): object;
|
||||||
|
}
|
||||||
@ -0,0 +1,56 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace PHPNative\Container\src\Exceptions;
|
||||||
|
|
||||||
|
use Exception;
|
||||||
|
use PHPNative\Container\src\ContainerLog;
|
||||||
|
use ReflectionClass;
|
||||||
|
|
||||||
|
final class CannotInstantiateDependencyException extends Exception
|
||||||
|
{
|
||||||
|
public function __construct(ReflectionClass $class, ContainerLog $containerLog)
|
||||||
|
{
|
||||||
|
$message = "Cannot resolve {$class->getName()} because it is not an instantiable class. Maybe it's missing an initializer class?" . PHP_EOL;
|
||||||
|
|
||||||
|
$stack = $containerLog->getStack();
|
||||||
|
|
||||||
|
if ($stack === []) {
|
||||||
|
parent::__construct($message);
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
$lastContext = $stack[array_key_last($stack)];
|
||||||
|
|
||||||
|
$i = 0;
|
||||||
|
|
||||||
|
foreach ($stack as $currentContext) {
|
||||||
|
$pipe = match (true) {
|
||||||
|
count($stack) > 1 && $i === 0 => '┌──',
|
||||||
|
count($stack) > 1 && $i === count($stack) - 1 => '└──',
|
||||||
|
count($stack) === 1 => ' ',
|
||||||
|
default => '├──',
|
||||||
|
};
|
||||||
|
|
||||||
|
$message .= PHP_EOL . "\t{$pipe} " . $currentContext;
|
||||||
|
|
||||||
|
$i++;
|
||||||
|
}
|
||||||
|
|
||||||
|
$currentDependency = $lastContext->currentDependency();
|
||||||
|
$currentDependencyName = (string)$currentDependency;
|
||||||
|
$firstPart = explode($currentDependencyName, (string)$lastContext)[0] ?? null;
|
||||||
|
$fillerSpaces = str_repeat(' ', strlen($firstPart) + 3);
|
||||||
|
$fillerArrows = str_repeat('▒', strlen($currentDependencyName));
|
||||||
|
$message .= PHP_EOL . "\t {$fillerSpaces}{$fillerArrows}";
|
||||||
|
|
||||||
|
$message .= PHP_EOL . PHP_EOL;
|
||||||
|
|
||||||
|
$message .= "Originally called in {$containerLog->getOrigin()}";
|
||||||
|
$message .= PHP_EOL;
|
||||||
|
|
||||||
|
parent::__construct($message);
|
||||||
|
}
|
||||||
|
}
|
||||||
336
src/PHPNative/Container/src/GenericContainer.php
Normal file
336
src/PHPNative/Container/src/GenericContainer.php
Normal file
@ -0,0 +1,336 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace PHPNative\Container\src;
|
||||||
|
|
||||||
|
use PHPNative\Container\Exceptions\CannotAutowireException;
|
||||||
|
use PHPNative\Container\src\Exceptions\CannotInstantiateDependencyException;
|
||||||
|
use ReflectionClass;
|
||||||
|
use ReflectionFunction;
|
||||||
|
use ReflectionIntersectionType;
|
||||||
|
use ReflectionMethod;
|
||||||
|
use ReflectionNamedType;
|
||||||
|
use ReflectionParameter;
|
||||||
|
use ReflectionUnionType;
|
||||||
|
use Throwable;
|
||||||
|
use function PHPNative\attribute;
|
||||||
|
|
||||||
|
final class GenericContainer implements Container
|
||||||
|
{
|
||||||
|
use HasInstance;
|
||||||
|
|
||||||
|
public function __construct(
|
||||||
|
private array $definitions = [],
|
||||||
|
private array $singletons = [],
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @template T of \PHPNative\Container\src\Initializer
|
||||||
|
* @var class-string<T> $initializers
|
||||||
|
*/
|
||||||
|
private array $initializers = [],
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @template T of \PHPNative\Container\src\DynamicInitializer
|
||||||
|
* @var class-string<T> $dynamicInitializers
|
||||||
|
*/
|
||||||
|
private array $dynamicInitializers = [],
|
||||||
|
private readonly ContainerLog $log = new InMemoryContainerLog(),
|
||||||
|
) {
|
||||||
|
}
|
||||||
|
|
||||||
|
public function setInitializers(array $initializers): void
|
||||||
|
{
|
||||||
|
$this->initializers = $initializers;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getInitializers(): array
|
||||||
|
{
|
||||||
|
return $this->initializers;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function register(string $className, callable $definition): self
|
||||||
|
{
|
||||||
|
$this->definitions[$className] = $definition;
|
||||||
|
|
||||||
|
return $this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function singleton(string $className, callable $definition): self
|
||||||
|
{
|
||||||
|
$this->definitions[$className] = function () use ($definition, $className) {
|
||||||
|
$instance = $definition($this);
|
||||||
|
|
||||||
|
$this->singletons[$className] = $instance;
|
||||||
|
|
||||||
|
return $instance;
|
||||||
|
};
|
||||||
|
|
||||||
|
return $this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function config(object $config): self
|
||||||
|
{
|
||||||
|
$this->singleton($config::class, fn () => $config);
|
||||||
|
|
||||||
|
return $this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function get(string $className, mixed ...$params): object
|
||||||
|
{
|
||||||
|
$this->log->startResolving();
|
||||||
|
|
||||||
|
return $this->resolve($className, ...$params);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function call(string|object $object, string $methodName, ...$params): mixed
|
||||||
|
{
|
||||||
|
$this->log->startResolving();
|
||||||
|
|
||||||
|
$object = is_string($object) ? $this->get($object) : $object;
|
||||||
|
|
||||||
|
$reflectionMethod = (new ReflectionClass($object))->getMethod($methodName);
|
||||||
|
|
||||||
|
$parameters = $this->autowireDependencies($reflectionMethod, $params);
|
||||||
|
|
||||||
|
return $reflectionMethod->invokeArgs($object, $parameters);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function addInitializer(ReflectionClass|string $initializerClass): Container
|
||||||
|
{
|
||||||
|
$initializerClass = $initializerClass instanceof ReflectionClass
|
||||||
|
? $initializerClass
|
||||||
|
: new ReflectionClass($initializerClass);
|
||||||
|
|
||||||
|
// First, we check whether this is a DynamicInitializer,
|
||||||
|
// which don't have a one-to-one mapping
|
||||||
|
if ($initializerClass->implementsInterface(DynamicInitializer::class)) {
|
||||||
|
$this->dynamicInitializers[] = $initializerClass->getName();
|
||||||
|
|
||||||
|
return $this;
|
||||||
|
}
|
||||||
|
|
||||||
|
// For normal Initializers, we'll use the return type
|
||||||
|
// to determine which dependency they resolve
|
||||||
|
$returnTypes = $initializerClass->getMethod('initialize')->getReturnType();
|
||||||
|
|
||||||
|
$returnTypes = match ($returnTypes::class) {
|
||||||
|
ReflectionNamedType::class => [$returnTypes],
|
||||||
|
ReflectionUnionType::class, ReflectionIntersectionType::class => $returnTypes->getTypes(),
|
||||||
|
};
|
||||||
|
|
||||||
|
/** @var ReflectionNamedType[] $returnTypes */
|
||||||
|
foreach ($returnTypes as $returnType) {
|
||||||
|
$this->initializers[$returnType->getName()] = $initializerClass->getName();
|
||||||
|
}
|
||||||
|
|
||||||
|
return $this;
|
||||||
|
}
|
||||||
|
|
||||||
|
private function resolve(string $className, mixed ...$params): object
|
||||||
|
{
|
||||||
|
// Check if the class has been registered as a singleton.
|
||||||
|
if ($instance = $this->singletons[$className] ?? null) {
|
||||||
|
$this->log->addContext(new Context(new ReflectionClass($className)));
|
||||||
|
|
||||||
|
return $instance;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if a callable has been registered to resolve this class.
|
||||||
|
if ($definition = $this->definitions[$className] ?? null) {
|
||||||
|
$this->log->addContext(new Context(new ReflectionFunction($definition)));
|
||||||
|
|
||||||
|
return $definition($this);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Next we check if any of our default initializers can initialize this class.
|
||||||
|
// If there's an initializer, we don't keep track of the log anymore,
|
||||||
|
// since initializers are outside the container's responsibility.
|
||||||
|
if ($initializer = $this->initializerFor($className)) {
|
||||||
|
$object = match (true) {
|
||||||
|
$initializer instanceof Initializer => $initializer->initialize($this),
|
||||||
|
$initializer instanceof DynamicInitializer => $initializer->initialize($className, $this),
|
||||||
|
};
|
||||||
|
// Check whether the initializer's result should be registered as a singleton
|
||||||
|
if (attribute(Singleton::class)->in($initializer::class)->first() !== null) {
|
||||||
|
$this->singleton($className, fn () => $object);
|
||||||
|
|
||||||
|
return $this->get($className);
|
||||||
|
}
|
||||||
|
|
||||||
|
return $object;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Finally, autowire the class.
|
||||||
|
return $this->autowire($className, ...$params);
|
||||||
|
}
|
||||||
|
|
||||||
|
private function initializerFor(string $className): null|Initializer|DynamicInitializer
|
||||||
|
{
|
||||||
|
// Initializers themselves can't be initialized,
|
||||||
|
// otherwise you'd end up with infinite loops
|
||||||
|
if (
|
||||||
|
is_a($className, Initializer::class, true)
|
||||||
|
|| is_a($className, DynamicInitializer::class, true)
|
||||||
|
) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($initializerClass = $this->initializers[$className] ?? null) {
|
||||||
|
return $this->resolve($initializerClass);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Loop through the registered initializers to see if
|
||||||
|
// we have something to handle this class.
|
||||||
|
foreach ($this->dynamicInitializers as $initializerClass) {
|
||||||
|
$initializer = $this->resolve($initializerClass);
|
||||||
|
|
||||||
|
if (! $initializer->canInitialize($className)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
return $initializer;
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
private function autowire(string $className, mixed ...$params): object
|
||||||
|
{
|
||||||
|
$reflectionClass = new ReflectionClass($className);
|
||||||
|
|
||||||
|
$constructor = $reflectionClass->getConstructor();
|
||||||
|
|
||||||
|
if (! $reflectionClass->isInstantiable()) {
|
||||||
|
throw new CannotInstantiateDependencyException($reflectionClass, $this->log);
|
||||||
|
}
|
||||||
|
|
||||||
|
return $constructor === null
|
||||||
|
// If there isn't a constructor, don't waste time
|
||||||
|
// trying to build it.
|
||||||
|
? $reflectionClass->newInstanceWithoutConstructor()
|
||||||
|
|
||||||
|
// Otherwise, use our autowireDependencies helper to automagically
|
||||||
|
// build up each parameter.
|
||||||
|
: $reflectionClass->newInstanceArgs(
|
||||||
|
$this->autowireDependencies($constructor, $params),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return ReflectionParameter[]
|
||||||
|
*/
|
||||||
|
private function autowireDependencies(ReflectionMethod $method, array $parameters = []): array
|
||||||
|
{
|
||||||
|
$this->log->addContext(new Context($method));
|
||||||
|
|
||||||
|
$dependencies = [];
|
||||||
|
|
||||||
|
// Build the class by iterating through its
|
||||||
|
// dependencies and resolving them.
|
||||||
|
foreach ($method->getParameters() as $parameter) {
|
||||||
|
$dependencies[] = $this->autowireDependency(
|
||||||
|
parameter: $parameter,
|
||||||
|
providedValue: $parameters[$parameter->getName()] ?? null,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return $dependencies;
|
||||||
|
}
|
||||||
|
|
||||||
|
private function autowireDependency(ReflectionParameter $parameter, mixed $providedValue = null): mixed
|
||||||
|
{
|
||||||
|
$this->log->addDependency(new Dependency($parameter));
|
||||||
|
|
||||||
|
$parameterType = $parameter->getType();
|
||||||
|
|
||||||
|
// If the parameter is a built-in type, immediately skip reflection
|
||||||
|
// stuff and attempt to give it a default or null value.
|
||||||
|
if ($parameterType instanceof ReflectionNamedType && $parameterType->isBuiltin()) {
|
||||||
|
return $this->autowireBuiltinDependency($parameter, $providedValue);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Convert the types to an array regardless, so we can handle
|
||||||
|
// union types and single types the same.
|
||||||
|
$types = match ($parameterType::class) {
|
||||||
|
ReflectionNamedType::class => [$parameterType],
|
||||||
|
ReflectionUnionType::class, ReflectionIntersectionType::class => $parameterType->getTypes(),
|
||||||
|
};
|
||||||
|
|
||||||
|
// Loop through each type until we hit a match.
|
||||||
|
foreach ($types as $type) {
|
||||||
|
try {
|
||||||
|
return $this->autowireObjectDependency($type, $providedValue);
|
||||||
|
} catch (Throwable $throwable) {
|
||||||
|
// We were unable to resolve the dependency for the last union
|
||||||
|
// type, so we are moving on to the next one. We hang onto
|
||||||
|
// the exception in case it is a circular reference.
|
||||||
|
$lastThrowable = $throwable;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// If the dependency has a default value, we do our best to prevent
|
||||||
|
// an error by using that.
|
||||||
|
if ($parameter->isDefaultValueAvailable()) {
|
||||||
|
return $parameter->getDefaultValue();
|
||||||
|
}
|
||||||
|
|
||||||
|
// At this point, there is nothing else we can do; we don't know
|
||||||
|
// how to autowire this dependency.
|
||||||
|
throw $lastThrowable ?? new CannotAutowireException($this->log);
|
||||||
|
}
|
||||||
|
|
||||||
|
private function autowireObjectDependency(ReflectionNamedType $type, mixed $providedValue): mixed
|
||||||
|
{
|
||||||
|
// If the provided value is of the right type,
|
||||||
|
// don't waste time autowiring, return it!
|
||||||
|
if (is_a($providedValue, $type->getName())) {
|
||||||
|
return $providedValue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// If we can successfully retrieve an instance
|
||||||
|
// of the necessary dependency, return it.
|
||||||
|
if ($instance = $this->resolve($type->getName())) {
|
||||||
|
return $instance;
|
||||||
|
}
|
||||||
|
|
||||||
|
// At this point, there is nothing else we can do; we don't know
|
||||||
|
// how to autowire this dependency.
|
||||||
|
throw new CannotAutowireException($this->log);
|
||||||
|
}
|
||||||
|
|
||||||
|
private function autowireBuiltinDependency(ReflectionParameter $parameter, mixed $providedValue): mixed
|
||||||
|
{
|
||||||
|
// Due to type coercion, the provided value may (or may not) work.
|
||||||
|
// Here we give up trying to do type work for people. If they
|
||||||
|
// didn't provide the right type, that's on them.
|
||||||
|
if ($providedValue !== null) {
|
||||||
|
return $providedValue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// If the dependency has a default value, we might as well
|
||||||
|
// use that at this point.
|
||||||
|
if ($parameter->isDefaultValueAvailable()) {
|
||||||
|
return $parameter->getDefaultValue();
|
||||||
|
}
|
||||||
|
|
||||||
|
// If the dependency's type is an array or variadic variable, we'll
|
||||||
|
// try to prevent an error by returning an empty array.
|
||||||
|
if (
|
||||||
|
$parameter->getType()?->getName() === 'array' ||
|
||||||
|
$parameter->isVariadic()
|
||||||
|
) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
// If the dependency's type allows null or is optional, we'll
|
||||||
|
// try to prevent an error by returning null.
|
||||||
|
if ($parameter->allowsNull() || $parameter->isOptional()) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// At this point, there is nothing else we can do; we don't know
|
||||||
|
// how to autowire this dependency.
|
||||||
|
throw new CannotAutowireException($this->log);
|
||||||
|
}
|
||||||
|
}
|
||||||
20
src/PHPNative/Container/src/HasInstance.php
Normal file
20
src/PHPNative/Container/src/HasInstance.php
Normal file
@ -0,0 +1,20 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace PHPNative\Container\src;
|
||||||
|
|
||||||
|
trait HasInstance
|
||||||
|
{
|
||||||
|
private static self $instance;
|
||||||
|
|
||||||
|
public static function instance(): self
|
||||||
|
{
|
||||||
|
return self::$instance;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static function setInstance(self $instance): void
|
||||||
|
{
|
||||||
|
self::$instance = $instance;
|
||||||
|
}
|
||||||
|
}
|
||||||
67
src/PHPNative/Container/src/InMemoryContainerLog.php
Normal file
67
src/PHPNative/Container/src/InMemoryContainerLog.php
Normal file
@ -0,0 +1,67 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace PHPNative\Container\src;
|
||||||
|
|
||||||
|
use Exception;
|
||||||
|
use Tempest\Container\Exceptions\CircularDependencyException;
|
||||||
|
|
||||||
|
final class InMemoryContainerLog implements ContainerLog
|
||||||
|
{
|
||||||
|
private string $origin = '';
|
||||||
|
|
||||||
|
public function __construct(
|
||||||
|
/** @var \PHPNative\Container\src\Context[] $stack */
|
||||||
|
private array $stack = [],
|
||||||
|
) {
|
||||||
|
}
|
||||||
|
|
||||||
|
public function startResolving(): ContainerLog
|
||||||
|
{
|
||||||
|
$trace = debug_backtrace(DEBUG_BACKTRACE_IGNORE_ARGS);
|
||||||
|
$this->origin = $trace[1]['file'] . ':' . $trace[1]['line'];
|
||||||
|
$this->stack = [];
|
||||||
|
|
||||||
|
return $this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function addContext(Context $context): ContainerLog
|
||||||
|
{
|
||||||
|
if (isset($this->stack[$context->getName()])) {
|
||||||
|
throw new CircularDependencyException($this, $context);
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->stack[$context->getName()] = $context;
|
||||||
|
|
||||||
|
return $this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function addDependency(Dependency $dependency): ContainerLog
|
||||||
|
{
|
||||||
|
$this->currentContext()->addDependency($dependency);
|
||||||
|
|
||||||
|
return $this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getStack(): array
|
||||||
|
{
|
||||||
|
return $this->stack;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function currentContext(): Context
|
||||||
|
{
|
||||||
|
return $this->stack[array_key_last($this->stack)]
|
||||||
|
?? throw new Exception("No current context found. That shoudn't happen. Aidan probably wrote a bug somewhere.");
|
||||||
|
}
|
||||||
|
|
||||||
|
public function currentDependency(): ?Dependency
|
||||||
|
{
|
||||||
|
return $this->currentContext()->currentDependency();
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getOrigin(): string
|
||||||
|
{
|
||||||
|
return $this->origin;
|
||||||
|
}
|
||||||
|
}
|
||||||
10
src/PHPNative/Container/src/Initializer.php
Normal file
10
src/PHPNative/Container/src/Initializer.php
Normal file
@ -0,0 +1,10 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace PHPNative\Container\src;
|
||||||
|
|
||||||
|
interface Initializer
|
||||||
|
{
|
||||||
|
public function initialize(Container $container): object;
|
||||||
|
}
|
||||||
12
src/PHPNative/Container/src/Singleton.php
Normal file
12
src/PHPNative/Container/src/Singleton.php
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace PHPNative\Container\src;
|
||||||
|
|
||||||
|
use Attribute;
|
||||||
|
|
||||||
|
#[Attribute]
|
||||||
|
final readonly class Singleton
|
||||||
|
{
|
||||||
|
}
|
||||||
17
src/PHPNative/Core/composer.json
Normal file
17
src/PHPNative/Core/composer.json
Normal file
@ -0,0 +1,17 @@
|
|||||||
|
{
|
||||||
|
"name": "phpnative/core",
|
||||||
|
"license": "MIT",
|
||||||
|
"require": {
|
||||||
|
"php": "^8.3"
|
||||||
|
},
|
||||||
|
"autoload": {
|
||||||
|
"psr-4": {
|
||||||
|
"PHPNative\\Core\\": "src"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"autoload-dev": {
|
||||||
|
"psr-4": {
|
||||||
|
"PHPNative\\Core\\Tests\\": "tests"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
12
src/PHPNative/Core/src/Window.php
Normal file
12
src/PHPNative/Core/src/Window.php
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace PHPNative\Core;
|
||||||
|
|
||||||
|
class Window
|
||||||
|
{
|
||||||
|
public function __construct()
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
}
|
||||||
17
src/PHPNative/Framework/composer.json
Normal file
17
src/PHPNative/Framework/composer.json
Normal file
@ -0,0 +1,17 @@
|
|||||||
|
{
|
||||||
|
"name": "phpnative/framework",
|
||||||
|
"license": "MIT",
|
||||||
|
"require": {
|
||||||
|
"php": "^8.3"
|
||||||
|
},
|
||||||
|
"autoload": {
|
||||||
|
"psr-4": {
|
||||||
|
"PHPNative\\Framework\\": "src"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"autoload-dev": {
|
||||||
|
"psr-4": {
|
||||||
|
"PHPNative\\Framework\\Tests\\": "tests"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
60
src/PHPNative/Framework/src/Application/Kernel.php
Normal file
60
src/PHPNative/Framework/src/Application/Kernel.php
Normal file
@ -0,0 +1,60 @@
|
|||||||
|
<?PHP
|
||||||
|
|
||||||
|
namespace PHPNative\Framework\src\Application;
|
||||||
|
|
||||||
|
use PHPNative\Container\src\Container;
|
||||||
|
use PHPNative\Container\src\GenericContainer;
|
||||||
|
use PHPNative\Framework\Application\ConfigBootstrap;
|
||||||
|
use PHPNative\Framework\Application\DiscoveryBootstrap;
|
||||||
|
use PHPNative\Framework\Application\DiscoveryLocationBootstrap;
|
||||||
|
|
||||||
|
final class Kernel
|
||||||
|
{
|
||||||
|
|
||||||
|
private $loop;
|
||||||
|
|
||||||
|
public function __construct(
|
||||||
|
public string $root
|
||||||
|
)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
public function run(): void
|
||||||
|
{
|
||||||
|
$this->init();
|
||||||
|
}
|
||||||
|
|
||||||
|
public function init(): void
|
||||||
|
{
|
||||||
|
$container = $this->createContainer();
|
||||||
|
|
||||||
|
$bootstraps = [
|
||||||
|
DiscoveryLocationBootstrap::class,
|
||||||
|
ConfigBootstrap::class,
|
||||||
|
DiscoveryBootstrap::class,
|
||||||
|
];
|
||||||
|
|
||||||
|
foreach ($bootstraps as $bootstrap) {
|
||||||
|
$container->get(
|
||||||
|
$bootstrap,
|
||||||
|
kernel: $this,
|
||||||
|
)->boot();
|
||||||
|
}
|
||||||
|
|
||||||
|
return $container;
|
||||||
|
}
|
||||||
|
|
||||||
|
private function createContainer(): Container
|
||||||
|
{
|
||||||
|
$container = new GenericContainer();
|
||||||
|
|
||||||
|
GenericContainer::setInstance($container);
|
||||||
|
|
||||||
|
$container
|
||||||
|
->singleton(self::class, fn () => $this)
|
||||||
|
->singleton(Container::class, fn () => $container)
|
||||||
|
;
|
||||||
|
return $container;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
10
src/PHPNative/Framework/src/Application/Window.php
Normal file
10
src/PHPNative/Framework/src/Application/Window.php
Normal file
@ -0,0 +1,10 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace PHPNative\Framework\src\Application;
|
||||||
|
|
||||||
|
use PHPNative\UI\View;
|
||||||
|
|
||||||
|
interface Window
|
||||||
|
{
|
||||||
|
public function getView(): View;
|
||||||
|
}
|
||||||
21
src/PHPNative/Framework/src/Discovery/Discovery.php
Normal file
21
src/PHPNative/Framework/src/Discovery/Discovery.php
Normal file
@ -0,0 +1,21 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace PHPNative\Framework\src\Discovery;
|
||||||
|
|
||||||
|
use PHPNative\Container\src\Container;
|
||||||
|
use ReflectionClass;
|
||||||
|
|
||||||
|
interface Discovery
|
||||||
|
{
|
||||||
|
public function discover(ReflectionClass $class): void;
|
||||||
|
|
||||||
|
public function hasCache(): bool;
|
||||||
|
|
||||||
|
public function storeCache(): void;
|
||||||
|
|
||||||
|
public function restoreCache(Container $container): void;
|
||||||
|
|
||||||
|
public function destroyCache(): void;
|
||||||
|
}
|
||||||
54
src/PHPNative/Framework/src/Discovery/DiscoveryDiscovery.php
Normal file
54
src/PHPNative/Framework/src/Discovery/DiscoveryDiscovery.php
Normal file
@ -0,0 +1,54 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace PHPNative\Framework\src\Discovery;
|
||||||
|
|
||||||
|
use PHPNative\AppConfig;
|
||||||
|
use PHPNative\Container\src\Container;
|
||||||
|
use ReflectionClass;
|
||||||
|
|
||||||
|
final readonly class DiscoveryDiscovery implements Discovery
|
||||||
|
{
|
||||||
|
public const CACHE_PATH = __DIR__ . '/discovery-discovery.cache.php';
|
||||||
|
|
||||||
|
public function __construct(
|
||||||
|
private AppConfig $appConfig,
|
||||||
|
) {
|
||||||
|
}
|
||||||
|
|
||||||
|
public function discover(ReflectionClass $class): void
|
||||||
|
{
|
||||||
|
if (
|
||||||
|
! $class->isInstantiable()
|
||||||
|
|| ! $class->implementsInterface(Discovery::class)
|
||||||
|
|| $class->getName() === self::class
|
||||||
|
) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->appConfig->discoveryClasses[] = $class->getName();
|
||||||
|
}
|
||||||
|
|
||||||
|
public function hasCache(): bool
|
||||||
|
{
|
||||||
|
return file_exists(self::CACHE_PATH);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function storeCache(): void
|
||||||
|
{
|
||||||
|
file_put_contents(self::CACHE_PATH, serialize($this->appConfig->discoveryClasses));
|
||||||
|
}
|
||||||
|
|
||||||
|
public function restoreCache(Container $container): void
|
||||||
|
{
|
||||||
|
$discoveryClasses = unserialize(file_get_contents(self::CACHE_PATH));
|
||||||
|
|
||||||
|
$this->appConfig->discoveryClasses = $discoveryClasses;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function destroyCache(): void
|
||||||
|
{
|
||||||
|
@unlink(self::CACHE_PATH);
|
||||||
|
}
|
||||||
|
}
|
||||||
14
src/PHPNative/Framework/src/Discovery/DiscoveryLocation.php
Normal file
14
src/PHPNative/Framework/src/Discovery/DiscoveryLocation.php
Normal file
@ -0,0 +1,14 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace PHPNative\Framework\src\Discovery;
|
||||||
|
|
||||||
|
final readonly class DiscoveryLocation
|
||||||
|
{
|
||||||
|
public function __construct(
|
||||||
|
public string $namespace,
|
||||||
|
public string $path,
|
||||||
|
) {
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,51 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace PHPNative\Framework\src\Discovery;
|
||||||
|
|
||||||
|
use PHPNative\Container\src\Container;
|
||||||
|
use PHPNative\Container\src\Initializer;
|
||||||
|
use ReflectionClass;
|
||||||
|
|
||||||
|
final readonly class InitializerDiscovery implements Discovery
|
||||||
|
{
|
||||||
|
private const CACHE_PATH = __DIR__ . '/initializer-discovery.cache.php';
|
||||||
|
|
||||||
|
public function __construct(
|
||||||
|
private Container $container,
|
||||||
|
) {
|
||||||
|
}
|
||||||
|
|
||||||
|
public function discover(ReflectionClass $class): void
|
||||||
|
{
|
||||||
|
if (
|
||||||
|
! $class->isInstantiable()
|
||||||
|
|| ! $class->implementsInterface(Initializer::class)
|
||||||
|
) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->container->addInitializer($class);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function hasCache(): bool
|
||||||
|
{
|
||||||
|
return file_exists(self::CACHE_PATH);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function storeCache(): void
|
||||||
|
{
|
||||||
|
file_put_contents(self::CACHE_PATH, serialize($this->container->getInitializers()));
|
||||||
|
}
|
||||||
|
|
||||||
|
public function restoreCache(Container $container): void
|
||||||
|
{
|
||||||
|
$initializers = unserialize(file_get_contents(self::CACHE_PATH));
|
||||||
|
|
||||||
|
$this->container->setInitializers($initializers);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function destroyCache(): void
|
||||||
|
{
|
||||||
|
@unlink(self::CACHE_PATH);
|
||||||
|
}
|
||||||
|
}
|
||||||
55
src/PHPNative/Framework/src/Loop/Timer.php
Normal file
55
src/PHPNative/Framework/src/Loop/Timer.php
Normal file
@ -0,0 +1,55 @@
|
|||||||
|
<?php
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace PHPNative\Framework\src\Loop;
|
||||||
|
|
||||||
|
class Timer
|
||||||
|
{
|
||||||
|
public const DEFAULT_UPDATE_RATE = 60;
|
||||||
|
|
||||||
|
public float $ops = 0;
|
||||||
|
|
||||||
|
private float $time;
|
||||||
|
|
||||||
|
private float $rate = 1;
|
||||||
|
|
||||||
|
public function __construct(int $rate = self::DEFAULT_UPDATE_RATE)
|
||||||
|
{
|
||||||
|
$this->rate($rate);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function touch(float $now = null): void
|
||||||
|
{
|
||||||
|
$this->time = $now ?? \microtime(true);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
public function rate(int $rate): self
|
||||||
|
{
|
||||||
|
$this->rate = $rate === 0 ? 0 : 1 / $rate;
|
||||||
|
|
||||||
|
return $this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function delta(float $now): float
|
||||||
|
{
|
||||||
|
return $now - $this->time;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function next(float $now): ?float
|
||||||
|
{
|
||||||
|
$delta = $this->delta($now);
|
||||||
|
|
||||||
|
if ($delta > 0) {
|
||||||
|
$this->ops = 1 / $delta;
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($delta > $this->rate) {
|
||||||
|
$this->time = $now;
|
||||||
|
|
||||||
|
return $delta;
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
16
src/PHPNative/Framework/src/PHPNative.php
Normal file
16
src/PHPNative/Framework/src/PHPNative.php
Normal file
@ -0,0 +1,16 @@
|
|||||||
|
<?PHP
|
||||||
|
|
||||||
|
namespace PHPNative\Framework\src;
|
||||||
|
|
||||||
|
use PHPNative\Framework\src\Application\Kernel;
|
||||||
|
|
||||||
|
final class PHPNative {
|
||||||
|
|
||||||
|
public static function boot(string $directory): Kernel
|
||||||
|
{
|
||||||
|
|
||||||
|
return new Kernel($directory);
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
17
src/PHPNative/Tailwind/composer.json
Normal file
17
src/PHPNative/Tailwind/composer.json
Normal file
@ -0,0 +1,17 @@
|
|||||||
|
{
|
||||||
|
"name": "phpnative/tailwind",
|
||||||
|
"license": "MIT",
|
||||||
|
"require": {
|
||||||
|
"php": "^8.3"
|
||||||
|
},
|
||||||
|
"autoload": {
|
||||||
|
"psr-4": {
|
||||||
|
"PHPNative\\Tailwind\\": "src"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"autoload-dev": {
|
||||||
|
"psr-4": {
|
||||||
|
"PHPNative\\Tailwind\\Tests\\": "tests"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
288
src/PHPNative/Tailwind/src/Data/colors.json
Normal file
288
src/PHPNative/Tailwind/src/Data/colors.json
Normal file
@ -0,0 +1,288 @@
|
|||||||
|
{
|
||||||
|
"slate": {
|
||||||
|
"50": "#f8fafc",
|
||||||
|
"100": "#f1f5f9",
|
||||||
|
"200": "#e2e8f0",
|
||||||
|
"300": "#cbd5e1",
|
||||||
|
"400": "#94a3b8",
|
||||||
|
"500": "#64748b",
|
||||||
|
"600": "#475569",
|
||||||
|
"700": "#334155",
|
||||||
|
"800": "#1e293b",
|
||||||
|
"900": "#0f172a",
|
||||||
|
"950": "#020617"
|
||||||
|
},
|
||||||
|
"gray": {
|
||||||
|
"50": "#f9fafb",
|
||||||
|
"100": "#f3f4f6",
|
||||||
|
"200": "#e5e7eb",
|
||||||
|
"300": "#d1d5db",
|
||||||
|
"400": "#9ca3af",
|
||||||
|
"500": "#6b7280",
|
||||||
|
"600": "#4b5563",
|
||||||
|
"700": "#374151",
|
||||||
|
"800": "#1f2937",
|
||||||
|
"900": "#111827",
|
||||||
|
"950": "#030712"
|
||||||
|
},
|
||||||
|
"zinc": {
|
||||||
|
"50": "#fafafa",
|
||||||
|
"100": "#f4f4f5",
|
||||||
|
"200": "#e4e4e7",
|
||||||
|
"300": "#d4d4d8",
|
||||||
|
"400": "#a1a1aa",
|
||||||
|
"500": "#71717a",
|
||||||
|
"600": "#52525b",
|
||||||
|
"700": "#3f3f46",
|
||||||
|
"800": "#27272a",
|
||||||
|
"900": "#18181b",
|
||||||
|
"950": "#09090b"
|
||||||
|
},
|
||||||
|
"neutral": {
|
||||||
|
"50": "#fafafa",
|
||||||
|
"100": "#f5f5f5",
|
||||||
|
"200": "#e5e5e5",
|
||||||
|
"300": "#d4d4d4",
|
||||||
|
"400": "#a3a3a3",
|
||||||
|
"500": "#737373",
|
||||||
|
"600": "#525252",
|
||||||
|
"700": "#404040",
|
||||||
|
"800": "#262626",
|
||||||
|
"900": "#171717",
|
||||||
|
"950": "#0a0a0a"
|
||||||
|
},
|
||||||
|
"stone": {
|
||||||
|
"50": "#fafaf9",
|
||||||
|
"100": "#f5f5f4",
|
||||||
|
"200": "#e7e5e4",
|
||||||
|
"300": "#d6d3d1",
|
||||||
|
"400": "#a8a29e",
|
||||||
|
"500": "#78716c",
|
||||||
|
"600": "#57534e",
|
||||||
|
"700": "#44403c",
|
||||||
|
"800": "#292524",
|
||||||
|
"900": "#1c1917",
|
||||||
|
"950": "#0c0a09"
|
||||||
|
},
|
||||||
|
"red": {
|
||||||
|
"50": "#fef2f2",
|
||||||
|
"100": "#fee2e2",
|
||||||
|
"200": "#fecaca",
|
||||||
|
"300": "#fca5a5",
|
||||||
|
"400": "#f87171",
|
||||||
|
"500": "#ef4444",
|
||||||
|
"600": "#dc2626",
|
||||||
|
"700": "#b91c1c",
|
||||||
|
"800": "#991b1b",
|
||||||
|
"900": "#7f1d1d",
|
||||||
|
"950": "#450a0a"
|
||||||
|
},
|
||||||
|
"orange": {
|
||||||
|
"50": "#fff7ed",
|
||||||
|
"100": "#ffedd5",
|
||||||
|
"200": "#fed7aa",
|
||||||
|
"300": "#fdba74",
|
||||||
|
"400": "#fb923c",
|
||||||
|
"500": "#f97316",
|
||||||
|
"600": "#ea580c",
|
||||||
|
"700": "#c2410c",
|
||||||
|
"800": "#9a3412",
|
||||||
|
"900": "#7c2d12",
|
||||||
|
"950": "#431407"
|
||||||
|
},
|
||||||
|
"amber": {
|
||||||
|
"50": "#fffbeb",
|
||||||
|
"100": "#fef3c7",
|
||||||
|
"200": "#fde68a",
|
||||||
|
"300": "#fcd34d",
|
||||||
|
"400": "#fbbf24",
|
||||||
|
"500": "#f59e0b",
|
||||||
|
"600": "#d97706",
|
||||||
|
"700": "#b45309",
|
||||||
|
"800": "#92400e",
|
||||||
|
"900": "#78350f",
|
||||||
|
"950": "#451a03"
|
||||||
|
},
|
||||||
|
"yellow": {
|
||||||
|
"50": "#fefce8",
|
||||||
|
"100": "#fef9c3",
|
||||||
|
"200": "#fef08a",
|
||||||
|
"300": "#fde047",
|
||||||
|
"400": "#facc15",
|
||||||
|
"500": "#eab308",
|
||||||
|
"600": "#ca8a04",
|
||||||
|
"700": "#a16207",
|
||||||
|
"800": "#854d0e",
|
||||||
|
"900": "#713f12",
|
||||||
|
"950": "#422006"
|
||||||
|
},
|
||||||
|
"lime": {
|
||||||
|
"50": "#f7fee7",
|
||||||
|
"100": "#ecfccb",
|
||||||
|
"200": "#d9f99d",
|
||||||
|
"300": "#bef264",
|
||||||
|
"400": "#a3e635",
|
||||||
|
"500": "#84cc16",
|
||||||
|
"600": "#65a30d",
|
||||||
|
"700": "#4d7c0f",
|
||||||
|
"800": "#3f6212",
|
||||||
|
"900": "#365314",
|
||||||
|
"950": "#1a2e05"
|
||||||
|
},
|
||||||
|
"green": {
|
||||||
|
"50": "#f0fdf4",
|
||||||
|
"100": "#dcfce7",
|
||||||
|
"200": "#bbf7d0",
|
||||||
|
"300": "#86efac",
|
||||||
|
"400": "#4ade80",
|
||||||
|
"500": "#22c55e",
|
||||||
|
"600": "#16a34a",
|
||||||
|
"700": "#15803d",
|
||||||
|
"800": "#166534",
|
||||||
|
"900": "#14532d",
|
||||||
|
"950": "#052e16"
|
||||||
|
},
|
||||||
|
"emerald": {
|
||||||
|
"50": "#ecfdf5",
|
||||||
|
"100": "#d1fae5",
|
||||||
|
"200": "#a7f3d0",
|
||||||
|
"300": "#6ee7b7",
|
||||||
|
"400": "#34d399",
|
||||||
|
"500": "#10b981",
|
||||||
|
"600": "#059669",
|
||||||
|
"700": "#047857",
|
||||||
|
"800": "#065f46",
|
||||||
|
"900": "#064e3b",
|
||||||
|
"950": "#022c22"
|
||||||
|
},
|
||||||
|
"teal": {
|
||||||
|
"50": "#f0fdfa",
|
||||||
|
"100": "#ccfbf1",
|
||||||
|
"200": "#99f6e4",
|
||||||
|
"300": "#5eead4",
|
||||||
|
"400": "#2dd4bf",
|
||||||
|
"500": "#14b8a6",
|
||||||
|
"600": "#0d9488",
|
||||||
|
"700": "#0f766e",
|
||||||
|
"800": "#115e59",
|
||||||
|
"900": "#134e4a",
|
||||||
|
"950": "#042f2e"
|
||||||
|
},
|
||||||
|
"cyan": {
|
||||||
|
"50": "#ecfeff",
|
||||||
|
"100": "#cffafe",
|
||||||
|
"200": "#a5f3fc",
|
||||||
|
"300": "#67e8f9",
|
||||||
|
"400": "#22d3ee",
|
||||||
|
"500": "#06b6d4",
|
||||||
|
"600": "#0891b2",
|
||||||
|
"700": "#0e7490",
|
||||||
|
"800": "#155e75",
|
||||||
|
"900": "#164e63",
|
||||||
|
"950": "#083344"
|
||||||
|
},
|
||||||
|
"sky": {
|
||||||
|
"50": "#f0f9ff",
|
||||||
|
"100": "#e0f2fe",
|
||||||
|
"200": "#bae6fd",
|
||||||
|
"300": "#7dd3fc",
|
||||||
|
"400": "#38bdf8",
|
||||||
|
"500": "#0ea5e9",
|
||||||
|
"600": "#0284c7",
|
||||||
|
"700": "#0369a1",
|
||||||
|
"800": "#075985",
|
||||||
|
"900": "#0c4a6e",
|
||||||
|
"950": "#082f49"
|
||||||
|
},
|
||||||
|
"blue": {
|
||||||
|
"50": "#eff6ff",
|
||||||
|
"100": "#dbeafe",
|
||||||
|
"200": "#bfdbfe",
|
||||||
|
"300": "#93c5fd",
|
||||||
|
"400": "#60a5fa",
|
||||||
|
"500": "#3b82f6",
|
||||||
|
"600": "#2563eb",
|
||||||
|
"700": "#1d4ed8",
|
||||||
|
"800": "#1e40af",
|
||||||
|
"900": "#1e3a8a",
|
||||||
|
"950": "#172554"
|
||||||
|
},
|
||||||
|
"indigo": {
|
||||||
|
"50": "#eef2ff",
|
||||||
|
"100": "#e0e7ff",
|
||||||
|
"200": "#c7d2fe",
|
||||||
|
"300": "#a5b4fc",
|
||||||
|
"400": "#818cf8",
|
||||||
|
"500": "#6366f1",
|
||||||
|
"600": "#4f46e5",
|
||||||
|
"700": "#4338ca",
|
||||||
|
"800": "#3730a3",
|
||||||
|
"900": "#312e81",
|
||||||
|
"950": "#1e1b4b"
|
||||||
|
},
|
||||||
|
"violet": {
|
||||||
|
"50": "#f5f3ff",
|
||||||
|
"100": "#ede9fe",
|
||||||
|
"200": "#ddd6fe",
|
||||||
|
"300": "#c4b5fd",
|
||||||
|
"400": "#a78bfa",
|
||||||
|
"500": "#8b5cf6",
|
||||||
|
"600": "#7c3aed",
|
||||||
|
"700": "#6d28d9",
|
||||||
|
"800": "#5b21b6",
|
||||||
|
"900": "#4c1d95",
|
||||||
|
"950": "#2e1065"
|
||||||
|
},
|
||||||
|
"purple": {
|
||||||
|
"50": "#faf5ff",
|
||||||
|
"100": "#f3e8ff",
|
||||||
|
"200": "#e9d5ff",
|
||||||
|
"300": "#d8b4fe",
|
||||||
|
"400": "#c084fc",
|
||||||
|
"500": "#a855f7",
|
||||||
|
"600": "#9333ea",
|
||||||
|
"700": "#7e22ce",
|
||||||
|
"800": "#6b21a8",
|
||||||
|
"900": "#581c87",
|
||||||
|
"950": "#3b0764"
|
||||||
|
},
|
||||||
|
"fuchsia": {
|
||||||
|
"50": "#fdf4ff",
|
||||||
|
"100": "#fae8ff",
|
||||||
|
"200": "#f5d0fe",
|
||||||
|
"300": "#f0abfc",
|
||||||
|
"400": "#e879f9",
|
||||||
|
"500": "#d946ef",
|
||||||
|
"600": "#c026d3",
|
||||||
|
"700": "#a21caf",
|
||||||
|
"800": "#86198f",
|
||||||
|
"900": "#701a75",
|
||||||
|
"950": "#4a044e"
|
||||||
|
},
|
||||||
|
"pink": {
|
||||||
|
"50": "#fdf2f8",
|
||||||
|
"100": "#fce7f3",
|
||||||
|
"200": "#fbcfe8",
|
||||||
|
"300": "#f9a8d4",
|
||||||
|
"400": "#f472b6",
|
||||||
|
"500": "#ec4899",
|
||||||
|
"600": "#db2777",
|
||||||
|
"700": "#be185d",
|
||||||
|
"800": "#9d174d",
|
||||||
|
"900": "#831843",
|
||||||
|
"950": "#500724"
|
||||||
|
},
|
||||||
|
"rose": {
|
||||||
|
"50": "#fff1f2",
|
||||||
|
"100": "#ffe4e6",
|
||||||
|
"200": "#fecdd3",
|
||||||
|
"300": "#fda4af",
|
||||||
|
"400": "#fb7185",
|
||||||
|
"500": "#f43f5e",
|
||||||
|
"600": "#e11d48",
|
||||||
|
"700": "#be123c",
|
||||||
|
"800": "#9f1239",
|
||||||
|
"900": "#881337",
|
||||||
|
"950": "#4c0519"
|
||||||
|
}
|
||||||
|
}
|
||||||
21
src/PHPNative/Tailwind/src/Parser/Background.php
Normal file
21
src/PHPNative/Tailwind/src/Parser/Background.php
Normal file
@ -0,0 +1,21 @@
|
|||||||
|
<?php
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace PHPNative\Tailwind\Parser;
|
||||||
|
|
||||||
|
class Background implements Parser
|
||||||
|
{
|
||||||
|
|
||||||
|
public static function parse(string $style): \PHPNative\Tailwind\Style\Background
|
||||||
|
{
|
||||||
|
$color = new \PHPNative\Tailwind\Style\Color();
|
||||||
|
|
||||||
|
preg_match_all('/bg-(.*)/', $style, $output_array);
|
||||||
|
if(count($output_array[0]) > 0) {
|
||||||
|
$colorStyle = $output_array[1][0];
|
||||||
|
$color = Color::parse($colorStyle);
|
||||||
|
|
||||||
|
}
|
||||||
|
return new \PHPNative\Tailwind\Style\Background($color);
|
||||||
|
}
|
||||||
|
}
|
||||||
34
src/PHPNative/Tailwind/src/Parser/Color.php
Normal file
34
src/PHPNative/Tailwind/src/Parser/Color.php
Normal file
@ -0,0 +1,34 @@
|
|||||||
|
<?php
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace PHPNative\Tailwind\Parser;
|
||||||
|
|
||||||
|
class Color implements Parser
|
||||||
|
{
|
||||||
|
|
||||||
|
public static function parse(string $style): \PHPNative\Tailwind\Style\Color
|
||||||
|
{
|
||||||
|
$red = 0;
|
||||||
|
$green = 0;
|
||||||
|
$blue = 0;
|
||||||
|
|
||||||
|
$data = json_decode(file_get_contents(__DIR__ . '/../Data/colors.json'), true);
|
||||||
|
|
||||||
|
preg_match_all('/(\w{1,8})/', $style, $output_array);
|
||||||
|
if(count($output_array[0]) > 0) {
|
||||||
|
$color = (string)$output_array[1][0];
|
||||||
|
list($red, $green, $blue) = sscanf($data[$color]['500'], "#%02x%02x%02x");
|
||||||
|
}
|
||||||
|
|
||||||
|
preg_match_all('/(\w{1,8})-(\d{1,3})/', $style, $output_array);
|
||||||
|
if(count($output_array[0]) > 0) {
|
||||||
|
$color = (string)$output_array[1][0];
|
||||||
|
$variant = (string)$output_array[2][0];
|
||||||
|
list($red, $green, $blue) = sscanf($data[$color][$variant], "#%02x%02x%02x");
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
return new \PHPNative\Tailwind\Style\Color($red, $green, $blue);
|
||||||
|
}
|
||||||
|
}
|
||||||
52
src/PHPNative/Tailwind/src/Parser/Padding.php
Normal file
52
src/PHPNative/Tailwind/src/Parser/Padding.php
Normal file
@ -0,0 +1,52 @@
|
|||||||
|
<?php
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace PHPNative\Tailwind\Parser;
|
||||||
|
|
||||||
|
class Padding implements Parser
|
||||||
|
{
|
||||||
|
|
||||||
|
public static function parse(string $style): \PHPNative\Tailwind\Style\Padding
|
||||||
|
{
|
||||||
|
$l = 0;
|
||||||
|
$r = 0;
|
||||||
|
$t = 0;
|
||||||
|
$b = 0;
|
||||||
|
|
||||||
|
preg_match_all('/p-(\d)/', $style, $output_array);
|
||||||
|
if(count($output_array[0]) > 0) {
|
||||||
|
$l = (int)$output_array[1][0];
|
||||||
|
$r = (int)$output_array[1][0];
|
||||||
|
$t = (int)$output_array[1][0];
|
||||||
|
$b = (int)$output_array[1][0];
|
||||||
|
}
|
||||||
|
preg_match_all('/px-(\d)/', $style, $output_array);
|
||||||
|
if(count($output_array[0]) > 0) {
|
||||||
|
$l = (int)$output_array[1][0];
|
||||||
|
$r = (int)$output_array[1][0];
|
||||||
|
}
|
||||||
|
preg_match_all('/py-(\d)/', $style, $output_array);
|
||||||
|
if(count($output_array[0]) > 0) {
|
||||||
|
$t = (int)$output_array[1][0];
|
||||||
|
$b = (int)$output_array[1][0];
|
||||||
|
}
|
||||||
|
preg_match_all('/pt-(\d)/', $style, $output_array);
|
||||||
|
if(count($output_array[0]) > 0) {
|
||||||
|
$t = (int)$output_array[1][0];
|
||||||
|
}
|
||||||
|
preg_match_all('/pb-(\d)/', $style, $output_array);
|
||||||
|
if(count($output_array[0]) > 0) {
|
||||||
|
$b = (int)$output_array[1][0];
|
||||||
|
}
|
||||||
|
preg_match_all('/pl-(\d)/', $style, $output_array);
|
||||||
|
if(count($output_array[0]) > 0) {
|
||||||
|
$l = (int)$output_array[1][0];
|
||||||
|
}
|
||||||
|
preg_match_all('/pr-(\d)/', $style, $output_array);
|
||||||
|
if(count($output_array[0]) > 0) {
|
||||||
|
$r = (int)$output_array[1][0];
|
||||||
|
}
|
||||||
|
|
||||||
|
return new \PHPNative\Tailwind\Style\Padding($l, $r, $t, $b);
|
||||||
|
}
|
||||||
|
}
|
||||||
11
src/PHPNative/Tailwind/src/Parser/Parser.php
Normal file
11
src/PHPNative/Tailwind/src/Parser/Parser.php
Normal file
@ -0,0 +1,11 @@
|
|||||||
|
<?php
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace PHPNative\Tailwind\Parser;
|
||||||
|
|
||||||
|
use PHPNative\Tailwind\Style\Style;
|
||||||
|
|
||||||
|
interface Parser
|
||||||
|
{
|
||||||
|
public static function parse(string $style): Style;
|
||||||
|
}
|
||||||
13
src/PHPNative/Tailwind/src/Style/Background.php
Normal file
13
src/PHPNative/Tailwind/src/Style/Background.php
Normal file
@ -0,0 +1,13 @@
|
|||||||
|
<?php
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace PHPNative\Tailwind\Style;
|
||||||
|
|
||||||
|
class Background implements Style
|
||||||
|
{
|
||||||
|
|
||||||
|
public function __construct(public Color $color = new Color())
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
11
src/PHPNative/Tailwind/src/Style/Color.php
Normal file
11
src/PHPNative/Tailwind/src/Style/Color.php
Normal file
@ -0,0 +1,11 @@
|
|||||||
|
<?php
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace PHPNative\Tailwind\Style;
|
||||||
|
|
||||||
|
class Color implements Style
|
||||||
|
{
|
||||||
|
public function __construct(public int $red = 255, public int $green = 255, public int $blue = 255, public int $alpha = 0)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
}
|
||||||
11
src/PHPNative/Tailwind/src/Style/Padding.php
Normal file
11
src/PHPNative/Tailwind/src/Style/Padding.php
Normal file
@ -0,0 +1,11 @@
|
|||||||
|
<?php
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace PHPNative\Tailwind\Style;
|
||||||
|
|
||||||
|
class Padding implements Style
|
||||||
|
{
|
||||||
|
public function __construct(public int $left = 0, public int $right = 0, public int $top = 0, public int $bottom = 0)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
}
|
||||||
8
src/PHPNative/Tailwind/src/Style/Style.php
Normal file
8
src/PHPNative/Tailwind/src/Style/Style.php
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
<?php
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace PHPNative\Tailwind\Style;
|
||||||
|
|
||||||
|
interface Style
|
||||||
|
{
|
||||||
|
}
|
||||||
17
src/PHPNative/Tailwind/src/StyleParser.php
Normal file
17
src/PHPNative/Tailwind/src/StyleParser.php
Normal file
@ -0,0 +1,17 @@
|
|||||||
|
<?php
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace PHPNative\Tailwind;
|
||||||
|
|
||||||
|
use PHPNative\Tailwind\Style\Padding;
|
||||||
|
use PHPNative\Tailwind\Style\Style;
|
||||||
|
|
||||||
|
class StyleParser
|
||||||
|
{
|
||||||
|
|
||||||
|
public static function parse($style): Style
|
||||||
|
{
|
||||||
|
return new Padding();
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
22
src/PHPNative/Tailwind/tests/BackgroundTest.php
Normal file
22
src/PHPNative/Tailwind/tests/BackgroundTest.php
Normal file
@ -0,0 +1,22 @@
|
|||||||
|
<?php
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace PHPNative\Tailwind\Tests;
|
||||||
|
|
||||||
|
use PHPNative\Tailwind\Parser\Background;
|
||||||
|
use PHPUnit\Framework\TestCase;
|
||||||
|
|
||||||
|
class BackgroundTest extends TestCase
|
||||||
|
{
|
||||||
|
|
||||||
|
public function testBackground(): void
|
||||||
|
{
|
||||||
|
$bg = Background::parse("bg-slate-300");
|
||||||
|
$this->assertInstanceOf(\PHPNative\Tailwind\Style\Background::class, $bg);
|
||||||
|
$this->assertSame(203, $bg->color->red);
|
||||||
|
$this->assertSame(213, $bg->color->green);
|
||||||
|
$this->assertSame(225, $bg->color->blue);
|
||||||
|
$this->assertSame(0, $bg->color->alpha);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
30
src/PHPNative/Tailwind/tests/ColorTest.php
Normal file
30
src/PHPNative/Tailwind/tests/ColorTest.php
Normal file
@ -0,0 +1,30 @@
|
|||||||
|
<?php
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace PHPNative\Tailwind\Tests;
|
||||||
|
|
||||||
|
use PHPNative\Tailwind\Parser\Color;
|
||||||
|
use PHPUnit\Framework\TestCase;
|
||||||
|
|
||||||
|
class ColorTest extends TestCase
|
||||||
|
{
|
||||||
|
public function testSimpleName(): void
|
||||||
|
{
|
||||||
|
$color = Color::parse("red");
|
||||||
|
self::assertInstanceOf(\PHPNative\Tailwind\Style\Color::class, $color);
|
||||||
|
self::assertSame(239, $color->red);
|
||||||
|
self::assertSame(68, $color->green);
|
||||||
|
self::assertSame(68, $color->blue);
|
||||||
|
self::assertSame(0, $color->alpha);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testNameVariant(): void
|
||||||
|
{
|
||||||
|
$color = Color::parse("lime-300");
|
||||||
|
self::assertInstanceOf(\PHPNative\Tailwind\Style\Color::class, $color);
|
||||||
|
self::assertSame(190, $color->red);
|
||||||
|
self::assertSame(242, $color->green);
|
||||||
|
self::assertSame(100, $color->blue);
|
||||||
|
self::assertSame(0, $color->alpha);
|
||||||
|
}
|
||||||
|
}
|
||||||
89
src/PHPNative/Tailwind/tests/PaddingTest.php
Normal file
89
src/PHPNative/Tailwind/tests/PaddingTest.php
Normal file
@ -0,0 +1,89 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace PHPNative\Tailwind\Tests;
|
||||||
|
|
||||||
|
use PHPNative\Tailwind\Parser\Padding;
|
||||||
|
use PHPUnit\Framework\TestCase;
|
||||||
|
|
||||||
|
class PaddingTest extends TestCase
|
||||||
|
{
|
||||||
|
public function testPaddingOverall(): void
|
||||||
|
{
|
||||||
|
$padding = Padding::parse("p-6");
|
||||||
|
self::assertInstanceOf(\PHPNative\Tailwind\Style\Padding::class, $padding);
|
||||||
|
self::assertSame(6, $padding->left);
|
||||||
|
self::assertSame(6, $padding->right);
|
||||||
|
self::assertSame(6, $padding->top);
|
||||||
|
self::assertSame(6, $padding->bottom);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testPaddingX(): void
|
||||||
|
{
|
||||||
|
$padding = Padding::parse("px-2");
|
||||||
|
self::assertInstanceOf(\PHPNative\Tailwind\Style\Padding::class, $padding);
|
||||||
|
self::assertSame(2, $padding->left);
|
||||||
|
self::assertSame(2, $padding->right);
|
||||||
|
self::assertSame(0, $padding->top);
|
||||||
|
self::assertSame(0, $padding->bottom);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testPaddingY(): void
|
||||||
|
{
|
||||||
|
$padding = Padding::parse("py-2");
|
||||||
|
self::assertInstanceOf(\PHPNative\Tailwind\Style\Padding::class, $padding);
|
||||||
|
self::assertSame(0, $padding->left);
|
||||||
|
self::assertSame(0, $padding->right);
|
||||||
|
self::assertSame(2, $padding->top);
|
||||||
|
self::assertSame(2, $padding->bottom);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testPaddingT(): void
|
||||||
|
{
|
||||||
|
$padding = Padding::parse("pt-3");
|
||||||
|
self::assertInstanceOf(\PHPNative\Tailwind\Style\Padding::class, $padding);
|
||||||
|
self::assertSame(0, $padding->left);
|
||||||
|
self::assertSame(0, $padding->right);
|
||||||
|
self::assertSame(3, $padding->top);
|
||||||
|
self::assertSame(0, $padding->bottom);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testPaddingB(): void
|
||||||
|
{
|
||||||
|
$padding = Padding::parse("pb-3");
|
||||||
|
self::assertInstanceOf(\PHPNative\Tailwind\Style\Padding::class, $padding);
|
||||||
|
self::assertSame(0, $padding->left);
|
||||||
|
self::assertSame(0, $padding->right);
|
||||||
|
self::assertSame(0, $padding->top);
|
||||||
|
self::assertSame(3, $padding->bottom);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testPaddingL(): void
|
||||||
|
{
|
||||||
|
$padding = Padding::parse("pl-3");
|
||||||
|
self::assertInstanceOf(\PHPNative\Tailwind\Style\Padding::class, $padding);
|
||||||
|
self::assertSame(3, $padding->left);
|
||||||
|
self::assertSame(0, $padding->right);
|
||||||
|
self::assertSame(0, $padding->top);
|
||||||
|
self::assertSame(0, $padding->bottom);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testPaddingR(): void
|
||||||
|
{
|
||||||
|
$padding = Padding::parse("pr-3");
|
||||||
|
self::assertInstanceOf(\PHPNative\Tailwind\Style\Padding::class, $padding);
|
||||||
|
self::assertSame(0, $padding->left);
|
||||||
|
self::assertSame(3, $padding->right);
|
||||||
|
self::assertSame(0, $padding->top);
|
||||||
|
self::assertSame(0, $padding->bottom);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testPaddingComplex(): void
|
||||||
|
{
|
||||||
|
$padding = Padding::parse("p-2 pl-1 pr-3");
|
||||||
|
self::assertInstanceOf(\PHPNative\Tailwind\Style\Padding::class, $padding);
|
||||||
|
self::assertSame(1, $padding->left);
|
||||||
|
self::assertSame(3, $padding->right);
|
||||||
|
self::assertSame(2, $padding->top);
|
||||||
|
self::assertSame(2, $padding->bottom);
|
||||||
|
}
|
||||||
|
}
|
||||||
8
src/PHPNative/UI/View.php
Normal file
8
src/PHPNative/UI/View.php
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace PHPNative\UI;
|
||||||
|
|
||||||
|
interface View
|
||||||
|
{
|
||||||
|
public function getWidget(): Widget;
|
||||||
|
}
|
||||||
8
src/PHPNative/UI/Widget.php
Normal file
8
src/PHPNative/UI/Widget.php
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace PHPNative\UI;
|
||||||
|
|
||||||
|
interface Widget
|
||||||
|
{
|
||||||
|
|
||||||
|
}
|
||||||
17
src/PHPNative/UI/composer.json
Normal file
17
src/PHPNative/UI/composer.json
Normal file
@ -0,0 +1,17 @@
|
|||||||
|
{
|
||||||
|
"name": "phpnative/ui",
|
||||||
|
"license": "MIT",
|
||||||
|
"require": {
|
||||||
|
"php": "^8.3"
|
||||||
|
},
|
||||||
|
"autoload": {
|
||||||
|
"psr-4": {
|
||||||
|
"PHPNative\\UI\\": "src"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"autoload-dev": {
|
||||||
|
"psr-4": {
|
||||||
|
"PHPNative\\UI\\Tests\\": "tests"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
10
src/PHPNative/UI/src/Trait/Action/Click.php
Normal file
10
src/PHPNative/UI/src/Trait/Action/Click.php
Normal file
@ -0,0 +1,10 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace PHPNative\UI\Trait\Action;
|
||||||
|
|
||||||
|
use Closure;
|
||||||
|
|
||||||
|
trait Click
|
||||||
|
{
|
||||||
|
public ?Closure $onClick = null;
|
||||||
|
}
|
||||||
10
src/PHPNative/UI/src/Trait/Style.php
Normal file
10
src/PHPNative/UI/src/Trait/Style.php
Normal file
@ -0,0 +1,10 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace PHPNative\UI\Trait;
|
||||||
|
|
||||||
|
trait Style
|
||||||
|
{
|
||||||
|
|
||||||
|
public string $style = "";
|
||||||
|
|
||||||
|
}
|
||||||
10
src/PHPNative/UI/src/View.php
Normal file
10
src/PHPNative/UI/src/View.php
Normal file
@ -0,0 +1,10 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace PHPNative\UI;
|
||||||
|
|
||||||
|
use PHPNative\UI\Trait\Style;
|
||||||
|
|
||||||
|
class View
|
||||||
|
{
|
||||||
|
use Style;
|
||||||
|
}
|
||||||
13
src/PHPNative/UI/src/Widget/Button.php
Normal file
13
src/PHPNative/UI/src/Widget/Button.php
Normal file
@ -0,0 +1,13 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace PHPNative\UI\Widget;
|
||||||
|
|
||||||
|
use PHPNative\UI\Trait\Action\Click;
|
||||||
|
use PHPNative\UI\Trait\Style;
|
||||||
|
|
||||||
|
class Button
|
||||||
|
{
|
||||||
|
use Style;
|
||||||
|
use Click;
|
||||||
|
|
||||||
|
}
|
||||||
10
src/PHPNative/UI/src/Widget/Container.php
Normal file
10
src/PHPNative/UI/src/Widget/Container.php
Normal file
@ -0,0 +1,10 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace PHPNative\UI\Widget;
|
||||||
|
|
||||||
|
use PHPNative\UI\Trait\Style;
|
||||||
|
|
||||||
|
class Container
|
||||||
|
{
|
||||||
|
use Style;
|
||||||
|
}
|
||||||
Loading…
Reference in New Issue
Block a user