This commit is contained in:
Thomas Peterson 2024-09-17 21:34:38 +02:00
parent e1ad5f5089
commit 7756cb774b
107 changed files with 3761 additions and 1017 deletions

88
.php-cs-fixer.dist.php Normal file
View File

@ -0,0 +1,88 @@
<?php
declare(strict_types=1);
$finder = Symfony\Component\Finder\Finder::create()
->in([
__DIR__ . '/src',
__DIR__ . '/tests',
])
->name('*.php')
->notName('*.cache.php')
->ignoreDotFiles(true)
->ignoreVCS(true);
return (new PhpCsFixer\Config())
->setCacheFile('.cache/fixer/cs-fixer.cache')
->setRules([
'@PSR12' => true,
'array_syntax' => ['syntax' => 'short'],
'ordered_imports' => ['sort_algorithm' => 'alpha'],
'no_unused_imports' => true,
'blank_line_between_import_groups' => false,
'single_import_per_statement' => true,
'no_leading_import_slash' => true,
'no_unneeded_import_alias' => true,
'fully_qualified_strict_types' => [
'import_symbols' => true,
],
'global_namespace_import' => [
'import_classes' => true,
'import_constants' => true,
'import_functions' => true,
],
'not_operator_with_successor_space' => true,
'trailing_comma_in_multiline' => true,
'phpdoc_scalar' => true,
'unary_operator_spaces' => true,
'binary_operator_spaces' => true,
'blank_line_before_statement' => [
'statements' => ['break', 'continue', 'declare', 'return', 'throw', 'try'],
],
'phpdoc_single_line_var_spacing' => true,
'phpdoc_var_without_name' => true,
'class_attributes_separation' => [
'elements' => [
'method' => 'one',
],
],
'method_argument_space' => [
'on_multiline' => 'ensure_fully_multiline',
'keep_multiple_spaces_after_comma' => true,
],
'single_trait_insert_per_statement' => true,
'declare_strict_types' => true,
'no_empty_comment' => true,
'no_empty_phpdoc' => true,
// Test styling
'php_unit_data_provider_name' => [
'prefix' => 'provide_',
'suffix' => '_cases',
],
'php_unit_data_provider_return_type' => true,
'php_unit_data_provider_static' => [
'force' => true,
],
'php_unit_dedicate_assert_internal_type' => true,
'php_unit_internal_class' => true,
'php_unit_method_casing' => [
'case' => 'snake_case',
],
'php_unit_expectation' => [
'target' => 'newest',
],
'php_unit_mock' => [
'target' => 'newest',
],
'php_unit_mock_short_will_return' => true,
'php_unit_set_up_tear_down_visibility' => true,
'php_unit_size_class' => true,
'php_unit_test_annotation' => [
'style' => 'prefix',
],
'php_unit_test_case_static_method_calls' => [
'call_type' => 'this',
],
])
->setFinder($finder);

BIN
assets/segoe-ui.ttf Normal file

Binary file not shown.

View File

@ -1,28 +1,7 @@
{ {
"name": "phpnative/framework", "name": "phpnative/framework",
"type": "library",
"license": "MIT", "license": "MIT",
"autoload": { "type": "library",
"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": [ "authors": [
{ {
"name": "Thomas Peterson", "name": "Thomas Peterson",
@ -30,8 +9,9 @@
} }
], ],
"require": { "require": {
"ext-parallel": "*",
"ext-sdl": "*", "ext-sdl": "*",
"ext-parallel": "*" "php": "^8.3"
}, },
"require-dev": { "require-dev": {
"friendsofphp/php-cs-fixer": "^3.21", "friendsofphp/php-cs-fixer": "^3.21",
@ -42,6 +22,40 @@
"spaze/phpstan-disallowed-calls": "^3.1", "spaze/phpstan-disallowed-calls": "^3.1",
"symplify/monorepo-builder": "^11.2" "symplify/monorepo-builder": "^11.2"
}, },
"replace": {
"phpnative/container": "self.version",
"phpnative/core": "self.version",
"phpnative/event": "self.version",
"phpnative/framework": "self.version",
"phpnative/renderer": "self.version",
"phpnative/support": "self.version",
"phpnative/tailwind": "self.version",
"phpnative/ui": "self.version"
},
"autoload": {
"psr-4": {
"PHPNative\\Container\\": "src/PHPNative/Container/src",
"PHPNative\\Core\\": "src/PHPNative/Core/src",
"PHPNative\\Event\\": "src/PHPNative/Event/src",
"PHPNative\\Framework\\": "src/PHPNative/Framework/src",
"PHPNative\\Renderer\\": "src/PHPNative/Renderer/src",
"PHPNative\\Support\\": "src/PHPNative/Support/src",
"PHPNative\\Tailwind\\": "src/PHPNative/Tailwind/src",
"PHPNative\\UI\\": "src/PHPNative/UI/src"
}
},
"autoload-dev": {
"psr-4": {
"PHPNative\\Container\\Tests\\": "src/PHPNative/Container/tests",
"PHPNative\\Core\\Tests\\": "src/PHPNative/Core/tests",
"PHPNative\\Event\\Tests\\": "src/PHPNative/Event/tests",
"PHPNative\\Framework\\Tests\\": "src/PHPNative/Framework/tests",
"PHPNative\\Renderer\\Tests\\": "src/PHPNative/Renderer/tests",
"PHPNative\\Support\\Tests\\": "src/PHPNative/Support/tests",
"PHPNative\\Tailwind\\Tests\\": "src/PHPNative/Tailwind/tests",
"PHPNative\\UI\\Tests\\": "src/PHPNative/UI/tests"
}
},
"scripts": { "scripts": {
"phpunit": "vendor/bin/phpunit --display-warnings --display-skipped --display-deprecations --display-errors --display-notices", "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", "coverage": "vendor/bin/phpunit --coverage-html build/reports/html --coverage-clover build/reports/clover.xml",
@ -51,7 +65,6 @@
"merge": "vendor/bin/monorepo-builder merge", "merge": "vendor/bin/monorepo-builder merge",
"qa": [ "qa": [
"composer merge", "composer merge",
"./tempest discovery:clear",
"vendor/bin/rector process", "vendor/bin/rector process",
"composer csfixer", "composer csfixer",
"composer phpunit", "composer phpunit",

75
rector.php Normal file
View File

@ -0,0 +1,75 @@
<?php
declare(strict_types=1);
use Rector\Arguments\Rector\ClassMethod\ArgumentAdderRector;
use Rector\Caching\ValueObject\Storage\FileCacheStorage;
use Rector\CodingStyle\Rector\Encapsed\EncapsedStringsToSprintfRector;
use Rector\Config\RectorConfig;
use Rector\DeadCode\Rector\ClassMethod\RemoveUnusedPublicMethodParameterRector;
use Rector\DeadCode\Rector\PropertyProperty\RemoveNullPropertyInitializationRector;
use Rector\DeadCode\Rector\Stmt\RemoveUnreachableStatementRector;
use Rector\Php70\Rector\StaticCall\StaticCallOnNonStaticToInstanceCallRector;
use Rector\Php74\Rector\Closure\ClosureToArrowFunctionRector;
use Rector\Php74\Rector\Property\RestoreDefaultNullToNullableTypePropertyRector;
use Rector\Php74\Rector\Ternary\ParenthesizeNestedTernaryRector;
use Rector\Php81\Rector\Array_\FirstClassCallableRector;
use Rector\Php81\Rector\FuncCall\NullToStrictStringFuncCallArgRector;
use Rector\Php81\Rector\Property\ReadOnlyPropertyRector;
use Rector\Php82\Rector\Class_\ReadOnlyClassRector;
use Rector\Php82\Rector\Param\AddSensitiveParameterAttributeRector;
use Rector\Php83\Rector\ClassMethod\AddOverrideAttributeToOverriddenMethodsRector;
use Rector\Php84\Rector\Param\ExplicitNullableParamTypeRector;
use Rector\TypeDeclaration\Rector\ArrowFunction\AddArrowFunctionReturnTypeRector;
use Rector\TypeDeclaration\Rector\ClassMethod\ReturnNeverTypeRector;
use Rector\TypeDeclaration\Rector\Closure\ClosureReturnTypeRector;
use Rector\TypeDeclaration\Rector\Empty_\EmptyOnNullableObjectToInstanceOfRector;
return RectorConfig::configure()
->withCache('./.cache/rector', FileCacheStorage::class)
->withPaths([
__DIR__ . '/src',
__DIR__ . '/tests',
])
->withConfiguredRule(AddSensitiveParameterAttributeRector::class, [
'sensitive_parameters' => [
'password',
'secret',
],
])
->withRules([
ParenthesizeNestedTernaryRector::class,
ExplicitNullableParamTypeRector::class,
])
->withSkip([
AddOverrideAttributeToOverriddenMethodsRector::class,
ArgumentAdderRector::class,
ClosureToArrowFunctionRector::class,
EmptyOnNullableObjectToInstanceOfRector::class,
FirstClassCallableRector::class,
NullToStrictStringFuncCallArgRector::class,
ReadOnlyClassRector::class,
ReadOnlyPropertyRector::class,
RemoveNullPropertyInitializationRector::class,
RemoveUnreachableStatementRector::class,
AddSensitiveParameterAttributeRector::class,
RemoveUnusedPublicMethodParameterRector::class,
RestoreDefaultNullToNullableTypePropertyRector::class,
ReturnNeverTypeRector::class,
StaticCallOnNonStaticToInstanceCallRector::class,
ClosureReturnTypeRector::class,
EncapsedStringsToSprintfRector::class,
AddArrowFunctionReturnTypeRector::class,
])
->withParallel(300, 10, 10)
->withPreparedSets(
codeQuality: false,
codingStyle: true,
privatization: true,
naming: false,
earlyReturn: true,
)
->withDeadCodeLevel(40)
->withMemoryLimit('3G')
->withPhpSets(php83: true)
->withTypeCoverageLevel(37);

View File

@ -2,32 +2,33 @@
declare(strict_types=1); declare(strict_types=1);
namespace PHPNative\Container\src; namespace PHPNative\Container;
use PHPNative\Support\Reflection\ClassReflector;
use PHPNative\Support\Reflection\MethodReflector;
use ReflectionClass; use ReflectionClass;
interface Container interface Container
{ {
public function register(string $className, callable $definition): self; public function register(string $className, callable $definition): self;
public function singleton(string $className, callable $definition): self; public function singleton(string $className, object|callable $definition, ?string $tag = null): self;
public function config(object $config): self; public function config(object $config): self;
/** /**
* @template TClassName * @template TClassName of object
* @param class-string<TClassName> $className * @param class-string<TClassName> $className
* @return TClassName * @return TClassName
*/ */
public function get(string $className, mixed ...$params): object; public function get(string $className, ?string $tag = null, mixed ...$params): object;
public function call(object $object, string $methodName, mixed ...$params): mixed; public function invoke(MethodReflector $method, mixed ...$params): mixed;
/** /**
* @template T of \PHPNative\Container\src\Initializer * @template T of \PHPNative\Container\Initializer
* @template U of \PHPNative\Container\src\DynamicInitializer * @template U of \PHPNative\Container\DynamicInitializer
* @param ReflectionClass|class-string<T>|class-string<U> $initializerClass * @param ClassReflector<T>|class-string<T>|class-string<U> $initializerClass
* @return self
*/ */
public function addInitializer(ReflectionClass|string $initializerClass): self; public function addInitializer(ClassReflector|string $initializerClass): self;
} }

View File

@ -1,25 +0,0 @@
<?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;
}

View File

@ -1,84 +0,0 @@
<?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,
)
);
}
}

View File

@ -2,86 +2,87 @@
declare(strict_types=1); declare(strict_types=1);
namespace PHPNative\Container\src; namespace PHPNative\Container;
use Closure;
use PHPNative\Support\Reflection\ClassReflector;
use PHPNative\Support\Reflection\FunctionReflector;
use PHPNative\Support\Reflection\MethodReflector;
use PHPNative\Support\Reflection\ParameterReflector;
use PHPNative\Support\Reflection\Reflector;
use PHPNative\Support\Reflection\TypeReflector;
use ReflectionClass;
use ReflectionIntersectionType;
use ReflectionNamedType;
use ReflectionParameter;
use ReflectionType;
use ReflectionUnionType;
final readonly class Dependency final readonly class Dependency
{ {
public function __construct( public function __construct(
public ReflectionParameter|ReflectionClass $reflector, public Reflector|Closure|string $dependency,
) { ) {
} }
public function getId(): string public function getName(): string
{ {
return $this->typeToString($this->getType()); return $this->resolveName($this->dependency);
} }
public function __toString(): string public function getShortName(): string
{ {
$typeToString = $this->typeToString($this->getType()); return $this->resolveShortName($this->dependency);
$parts = explode('\\', $typeToString);
$typeToString = $parts[array_key_last($parts)];
return implode(
' ',
array_filter([
$typeToString,
'$' . $this->reflector->getName(),
]),
);
} }
private function getType(): string|ReflectionType public function equals(self $other): bool
{ {
return match($this->reflector::class) { return $this->getName() === $other->getName();
ReflectionParameter::class => $this->reflector->getType(), }
ReflectionClass::class => $this->reflector->getName(),
public function getTypeName(): string
{
$dependency = $this->dependency;
if (is_string($dependency)) {
$parts = explode('\\', $dependency);
return $parts[array_key_last($parts)];
}
return match($dependency::class) {
ClassReflector::class => $dependency->getType()->getShortName(),
MethodReflector::class => $dependency->getDeclaringClass()->getType()->getShortName(),
ParameterReflector::class => $dependency->getType()->getShortName(),
TypeReflector::class => $dependency->getShortName(),
default => 'unknown',
}; };
} }
private function typeToString(string|ReflectionType|null $type): ?string private function resolveName(Reflector|Closure|string $dependency): string
{ {
if ($type === null) { if (is_string($dependency)) {
return null; return $dependency;
} }
if (is_string($type)) { return match($dependency::class) {
return $type; FunctionReflector::class => $dependency->getName() . ' in ' . $dependency->getFileName() . ':' . $dependency->getStartLine(),
} ClassReflector::class => $dependency->getName(),
MethodReflector::class => $dependency->getDeclaringClass()->getName() . '::' . $dependency->getName(),
return match($type::class) { ParameterReflector::class => $dependency->getType()->getName(),
ReflectionIntersectionType::class => $this->intersectionTypeToString($type), TypeReflector::class => $dependency->getName(),
ReflectionNamedType::class => $type->getName(), default => 'unknown',
ReflectionUnionType::class => $this->unionTypeToString($type),
}; };
} }
private function intersectionTypeToString(ReflectionIntersectionType $type): string private function resolveShortName(Reflector|Closure|string $dependency): string
{ {
return implode( if (is_string($dependency)) {
'&', return $dependency;
array_map( }
fn (ReflectionType $subType) => $this->typeToString($subType),
$type->getTypes(),
),
);
}
private function unionTypeToString(ReflectionUnionType $type): string return match($dependency::class) {
{ FunctionReflector::class => $dependency->getShortName() . ' in ' . $dependency->getFileName() . ':' . $dependency->getStartLine(),
return implode( ClassReflector::class => $dependency->getShortName(),
'|', MethodReflector::class => $dependency->getShortName(),
array_map( ParameterReflector::class => $dependency->getType()->getShortName(),
fn (ReflectionType $subType) => $this->typeToString($subType), TypeReflector::class => $dependency->getShortName(),
$type->getTypes(), default => 'unknown',
), };
);
} }
} }

View File

@ -0,0 +1,58 @@
<?php
namespace PHPNative\Container;
use Closure;
use PHPNative\Container\Exceptions\CircularDependencyException;
use PHPNative\Support\Reflection\Reflector;
final class DependencyChain
{
/**
* @var \PHPNative\Container\Dependency[]
*/
private array $dependencies = [];
public function __construct(private string $origin)
{
}
public function add(Reflector|Closure|string $dependency): self
{
$dependency = new Dependency($dependency);
if (isset($this->dependencies[$dependency->getName()])) {
throw new CircularDependencyException($this, $dependency);
}
$this->dependencies[$dependency->getName()] = $dependency;
return $this;
}
public function first(): Dependency
{
return $this->dependencies[array_key_first($this->dependencies)];
}
public function last(): Dependency
{
return $this->dependencies[array_key_last($this->dependencies)];
}
/** @return \PHPNative\Container\Dependency[] */
public function all(): array
{
return $this->dependencies;
}
public function getOrigin(): string
{
return $this->origin;
}
public function clone(): self
{
return clone $this;
}
}

View File

@ -2,7 +2,7 @@
declare(strict_types=1); declare(strict_types=1);
namespace PHPNative\Container\src; namespace PHPNative\Container;
interface DynamicInitializer interface DynamicInitializer
{ {

View File

@ -0,0 +1,51 @@
<?php
declare(strict_types=1);
namespace PHPNative\Container\Exceptions;
use Exception;
use PHPNative\Container\Dependency;
use PHPNative\Container\DependencyChain;
final class CannotAutowireException extends Exception
{
public function __construct(DependencyChain $chain, Dependency $brokenDependency)
{
$stack = $chain->all();
$firstDependency = $chain->first();
$message = PHP_EOL . PHP_EOL . "Cannot autowire {$firstDependency->getName()} because {$brokenDependency->getName()} cannot be resolved" . PHP_EOL;
$i = 0;
foreach ($stack as $currentDependency) {
$pipe = match ($i) {
0 => '┌──',
count($stack) - 1 => '└──',
default => '├──',
};
$message .= PHP_EOL . "\t{$pipe} " . $currentDependency->getShortName();
$i++;
}
$selectionLine = preg_replace_callback(
pattern: '/(?<prefix>(.*))(?<selection>'. $brokenDependency->getTypeName() .'\s\$\w+)(.*)/',
callback: function ($matches) {
return str_repeat(' ', strlen($matches['prefix']) + 4)
. str_repeat('▒', strlen($matches['selection']));
},
subject: $chain->last()->getShortName(),
);
$message .= PHP_EOL;
$message .= "\t{$selectionLine}";
$message .= PHP_EOL;
$message .= "Originally called in {$chain->getOrigin()}";
$message .= PHP_EOL;
parent::__construct($message);
}
}

View File

@ -2,19 +2,20 @@
declare(strict_types=1); declare(strict_types=1);
namespace PHPNative\Container\src\Exceptions; namespace PHPNative\Container\Exceptions;
use Exception; use Exception;
use PHPNative\Container\src\ContainerLog; use PHPNative\Container\DependencyChain;
use PHPNative\Support\Reflection\ClassReflector;
use ReflectionClass; use ReflectionClass;
final class CannotInstantiateDependencyException extends Exception final class CannotInstantiateDependencyException extends Exception
{ {
public function __construct(ReflectionClass $class, ContainerLog $containerLog) public function __construct(ClassReflector $class, DependencyChain $chain)
{ {
$message = "Cannot resolve {$class->getName()} because it is not an instantiable class. Maybe it's missing an initializer class?" . PHP_EOL; $message = "Cannot resolve {$class->getName()} because it is not an instantiable class. Maybe it's missing an initializer class?" . PHP_EOL;
$stack = $containerLog->getStack(); $stack = $chain->all();
if ($stack === []) { if ($stack === []) {
parent::__construct($message); parent::__construct($message);
@ -22,11 +23,9 @@ final class CannotInstantiateDependencyException extends Exception
return; return;
} }
$lastContext = $stack[array_key_last($stack)];
$i = 0; $i = 0;
foreach ($stack as $currentContext) { foreach ($stack as $currentDependency) {
$pipe = match (true) { $pipe = match (true) {
count($stack) > 1 && $i === 0 => '┌──', count($stack) > 1 && $i === 0 => '┌──',
count($stack) > 1 && $i === count($stack) - 1 => '└──', count($stack) > 1 && $i === count($stack) - 1 => '└──',
@ -34,23 +33,24 @@ final class CannotInstantiateDependencyException extends Exception
default => '├──', default => '├──',
}; };
$message .= PHP_EOL . "\t{$pipe} " . $currentContext; $message .= PHP_EOL . "\t{$pipe} " . $currentDependency->getShortName();
$i++; $i++;
} }
$currentDependency = $lastContext->currentDependency(); $lastDependency = $chain->last();
$currentDependencyName = (string)$currentDependency; // $currentDependencyName = $lastDependency->getShortName();
$firstPart = explode($currentDependencyName, (string)$lastContext)[0] ?? null; // $firstPart = explode($currentDependencyName, (string)$lastDependency)[0] ?? null;
$fillerSpaces = str_repeat(' ', strlen($firstPart) + 3); // $fillerSpaces = str_repeat(' ', strlen($firstPart) + 3);
$fillerArrows = str_repeat('▒', strlen($currentDependencyName)); // $fillerArrows = str_repeat('▒', strlen($currentDependencyName));
$message .= PHP_EOL . "\t {$fillerSpaces}{$fillerArrows}"; // $message .= PHP_EOL . "\t {$fillerSpaces}{$fillerArrows}";
//
// $message .= PHP_EOL . PHP_EOL;
$message .= PHP_EOL . PHP_EOL; $message .= "Originally called in {$chain->getOrigin()}";
$message .= "Originally called in {$containerLog->getOrigin()}";
$message .= PHP_EOL; $message .= PHP_EOL;
parent::__construct($message); parent::__construct($message);
} }
} }

View File

@ -2,51 +2,62 @@
declare(strict_types=1); declare(strict_types=1);
namespace PHPNative\Container\src; namespace PHPNative\Container;
use ArrayIterator;
use Closure;
use PHPNative\Container\Exceptions\CannotAutowireException; use PHPNative\Container\Exceptions\CannotAutowireException;
use PHPNative\Container\src\Exceptions\CannotInstantiateDependencyException; use PHPNative\Container\Exceptions\CannotInstantiateDependencyException;
use ReflectionClass; use PHPNative\Container\Exceptions\CannotResolveTaggedDependency;
use ReflectionFunction; use PHPNative\Support\Reflection\ClassReflector;
use ReflectionIntersectionType; use PHPNative\Support\Reflection\FunctionReflector;
use ReflectionMethod; use PHPNative\Support\Reflection\MethodReflector;
use ReflectionNamedType; use PHPNative\Support\Reflection\ParameterReflector;
use ReflectionParameter; use PHPNative\Support\Reflection\TypeReflector;
use ReflectionUnionType;
use Throwable; use Throwable;
use function PHPNative\attribute;
final class GenericContainer implements Container final class GenericContainer implements Container
{ {
use HasInstance; use HasInstance;
public function __construct( public function __construct(
private array $definitions = [], /** @var ArrayIterator<array-key, mixed> $definitions */
private array $singletons = [], private ArrayIterator $definitions = new ArrayIterator(),
/** /** @var ArrayIterator<array-key, mixed> $singletons */
* @template T of \PHPNative\Container\src\Initializer private ArrayIterator $singletons = new ArrayIterator(),
* @var class-string<T> $initializers
*/
private array $initializers = [],
/** /** @var ArrayIterator<array-key, class-string> $initializers */
* @template T of \PHPNative\Container\src\DynamicInitializer private ArrayIterator $initializers = new ArrayIterator(),
* @var class-string<T> $dynamicInitializers
*/ /** @var ArrayIterator<array-key, class-string> $dynamicInitializers */
private array $dynamicInitializers = [], private ArrayIterator $dynamicInitializers = new ArrayIterator(),
private readonly ContainerLog $log = new InMemoryContainerLog(), private ?DependencyChain $chain = null,
) { ) {
} }
public function setInitializers(array $initializers): void public function setInitializers(array $initializers): self
{ {
$this->initializers = $initializers; $this->initializers = new ArrayIterator($initializers);
return $this;
}
public function setDynamicInitializers(array $dynamicInitializers): self
{
$this->dynamicInitializers = new ArrayIterator($dynamicInitializers);
return $this;
} }
public function getInitializers(): array public function getInitializers(): array
{ {
return $this->initializers; return $this->initializers->getArrayCopy();
}
public function getDynamicInitializers(): array
{
return $this->dynamicInitializers->getArrayCopy();
} }
public function register(string $className, callable $definition): self public function register(string $className, callable $definition): self
@ -56,136 +67,154 @@ final class GenericContainer implements Container
return $this; return $this;
} }
public function singleton(string $className, callable $definition): self public function singleton(string $className, object|callable $definition, ?string $tag = null): self
{ {
$this->definitions[$className] = function () use ($definition, $className) { $className = $this->resolveTaggedName($className, $tag);
$instance = $definition($this);
$this->singletons[$className] = $instance; $this->singletons[$className] = $definition;
return $instance;
};
return $this; return $this;
} }
public function config(object $config): self public function config(object $config): self
{ {
$this->singleton($config::class, fn () => $config); $this->singleton($config::class, $config);
return $this; return $this;
} }
public function get(string $className, mixed ...$params): object public function get(string $className, ?string $tag = null, mixed ...$params): object
{ {
$this->log->startResolving(); $this->resolveChain();
return $this->resolve($className, ...$params); $dependency = $this->resolve(
className: $className,
tag: $tag,
params: $params,
);
$this->stopChain();
return $dependency;
} }
public function call(string|object $object, string $methodName, ...$params): mixed public function invoke(MethodReflector $method, mixed ...$params): mixed
{ {
$this->log->startResolving(); $this->resolveChain();
$object = is_string($object) ? $this->get($object) : $object; $object = $this->get($method->getDeclaringClass()->getName());
$reflectionMethod = (new ReflectionClass($object))->getMethod($methodName); $parameters = $this->autowireDependencies($method, $params);
$parameters = $this->autowireDependencies($reflectionMethod, $params); $this->stopChain();
return $reflectionMethod->invokeArgs($object, $parameters); return $method->invokeArgs($object, $parameters);
} }
public function addInitializer(ReflectionClass|string $initializerClass): Container public function addInitializer(ClassReflector|string $initializerClass): Container
{ {
$initializerClass = $initializerClass instanceof ReflectionClass if (! $initializerClass instanceof ClassReflector) {
? $initializerClass $initializerClass = new ClassReflector($initializerClass);
: new ReflectionClass($initializerClass); }
// First, we check whether this is a DynamicInitializer, // First, we check whether this is a DynamicInitializer,
// which don't have a one-to-one mapping // which don't have a one-to-one mapping
if ($initializerClass->implementsInterface(DynamicInitializer::class)) { if ($initializerClass->getType()->matches(DynamicInitializer::class)) {
$this->dynamicInitializers[] = $initializerClass->getName(); $this->dynamicInitializers[] = $initializerClass->getName();
return $this; return $this;
} }
$initializeMethod = $initializerClass->getMethod('initialize');
// We resolve the optional Tag attribute from this initializer class
$singleton = $initializeMethod->getAttribute(Singleton::class);
// For normal Initializers, we'll use the return type // For normal Initializers, we'll use the return type
// to determine which dependency they resolve // to determine which dependency they resolve
$returnTypes = $initializerClass->getMethod('initialize')->getReturnType(); $returnType = $initializeMethod->getReturnType();
$returnTypes = match ($returnTypes::class) { foreach ($returnType->split() as $type) {
ReflectionNamedType::class => [$returnTypes], $this->initializers[$this->resolveTaggedName($type->getName(), $singleton?->tag)] = $initializerClass->getName();
ReflectionUnionType::class, ReflectionIntersectionType::class => $returnTypes->getTypes(),
};
/** @var ReflectionNamedType[] $returnTypes */
foreach ($returnTypes as $returnType) {
$this->initializers[$returnType->getName()] = $initializerClass->getName();
} }
return $this; return $this;
} }
private function resolve(string $className, mixed ...$params): object private function resolve(string $className, ?string $tag = null, mixed ...$params): object
{ {
$class = new ClassReflector($className);
$dependencyName = $this->resolveTaggedName($className, $tag);
// Check if the class has been registered as a singleton. // Check if the class has been registered as a singleton.
if ($instance = $this->singletons[$className] ?? null) { if ($instance = $this->singletons[$dependencyName] ?? null) {
$this->log->addContext(new Context(new ReflectionClass($className))); if ($instance instanceof Closure) {
$instance = $instance($this);
$this->singletons[$className] = $instance;
}
$this->resolveChain()->add($class);
return $instance; return $instance;
} }
// Check if a callable has been registered to resolve this class. // Check if a callable has been registered to resolve this class.
if ($definition = $this->definitions[$className] ?? null) { if ($definition = $this->definitions[$dependencyName] ?? null) {
$this->log->addContext(new Context(new ReflectionFunction($definition))); $this->resolveChain()->add(new FunctionReflector($definition));
return $definition($this); return $definition($this);
} }
// Next we check if any of our default initializers can initialize this class. // 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, if (($initializer = $this->initializerFor($class, $tag)) !== null) {
// since initializers are outside the container's responsibility. $initializerClass = new ClassReflector($initializer);
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); $this->resolveChain()->add($initializerClass);
$object = match (true) {
$initializer instanceof Initializer => $initializer->initialize($this->clone()),
$initializer instanceof DynamicInitializer => $initializer->initialize($class, $this->clone()),
};
$singleton = $initializerClass->getAttribute(Singleton::class)
?? $initializerClass->getMethod('initialize')->getAttribute(Singleton::class);
if ($singleton !== null) {
$this->singleton($className, $object, $tag);
} }
return $object; return $object;
} }
// If we're requesting a tagged dependency and haven't resolved it at this point, something's wrong
if ($tag) {
throw new CannotResolveTaggedDependency($this->chain, new Dependency($className), $tag);
}
// Finally, autowire the class. // Finally, autowire the class.
return $this->autowire($className, ...$params); return $this->autowire($className, ...$params);
} }
private function initializerFor(string $className): null|Initializer|DynamicInitializer private function initializerFor(ClassReflector $class, ?string $tag = null): null|Initializer|DynamicInitializer
{ {
// Initializers themselves can't be initialized, // Initializers themselves can't be initialized,
// otherwise you'd end up with infinite loops // otherwise you'd end up with infinite loops
if ( if ($class->getType()->matches(Initializer::class) || $class->getType()->matches(DynamicInitializer::class)) {
is_a($className, Initializer::class, true)
|| is_a($className, DynamicInitializer::class, true)
) {
return null; return null;
} }
if ($initializerClass = $this->initializers[$className] ?? null) { if ($initializerClass = $this->initializers[$this->resolveTaggedName($class, $tag)] ?? null) {
return $this->resolve($initializerClass); return $this->resolve($initializerClass);
} }
// Loop through the registered initializers to see if // Loop through the registered initializers to see if
// we have something to handle this class. // we have something to handle this class.
foreach ($this->dynamicInitializers as $initializerClass) { foreach ($this->dynamicInitializers as $initializerClass) {
/** @var DynamicInitializer $initializer */
$initializer = $this->resolve($initializerClass); $initializer = $this->resolve($initializerClass);
if (! $initializer->canInitialize($className)) { if (! $initializer->canInitialize($class)) {
continue; continue;
} }
@ -197,40 +226,51 @@ final class GenericContainer implements Container
private function autowire(string $className, mixed ...$params): object private function autowire(string $className, mixed ...$params): object
{ {
$reflectionClass = new ReflectionClass($className); $classReflector = new ClassReflector($className);
$constructor = $reflectionClass->getConstructor(); $constructor = $classReflector->getConstructor();
if (! $reflectionClass->isInstantiable()) { if (! $classReflector->isInstantiable()) {
throw new CannotInstantiateDependencyException($reflectionClass, $this->log); throw new CannotInstantiateDependencyException($classReflector, $this->chain);
} }
return $constructor === null $instance = $constructor === null
// If there isn't a constructor, don't waste time // If there isn't a constructor, don't waste time
// trying to build it. // trying to build it.
? $reflectionClass->newInstanceWithoutConstructor() ? $classReflector->newInstanceWithoutConstructor()
// Otherwise, use our autowireDependencies helper to automagically // Otherwise, use our autowireDependencies helper to automagically
// build up each parameter. // build up each parameter.
: $reflectionClass->newInstanceArgs( : $classReflector->newInstanceArgs(
$this->autowireDependencies($constructor, $params), $this->autowireDependencies($constructor, $params),
); );
if (
! $classReflector->getType()->matches(Initializer::class)
&& ! $classReflector->getType()->matches(DynamicInitializer::class)
&& $classReflector->hasAttribute(Singleton::class)
) {
$this->singleton($className, $instance);
}
return $instance;
} }
/** /**
* @return ReflectionParameter[] * @return ParameterReflector[]
*/ */
private function autowireDependencies(ReflectionMethod $method, array $parameters = []): array private function autowireDependencies(MethodReflector $method, array $parameters = []): array
{ {
$this->log->addContext(new Context($method)); $this->resolveChain()->add($method);
$dependencies = []; $dependencies = [];
// Build the class by iterating through its // Build the class by iterating through its
// dependencies and resolving them. // dependencies and resolving them.
foreach ($method->getParameters() as $parameter) { foreach ($method->getParameters() as $parameter) {
$dependencies[] = $this->autowireDependency( $dependencies[] = $this->clone()->autowireDependency(
parameter: $parameter, parameter: $parameter,
tag: $parameter->getAttribute(Tag::class)?->name,
providedValue: $parameters[$parameter->getName()] ?? null, providedValue: $parameters[$parameter->getName()] ?? null,
); );
} }
@ -238,29 +278,24 @@ final class GenericContainer implements Container
return $dependencies; return $dependencies;
} }
private function autowireDependency(ReflectionParameter $parameter, mixed $providedValue = null): mixed private function autowireDependency(ParameterReflector $parameter, ?string $tag, mixed $providedValue = null): mixed
{ {
$this->log->addDependency(new Dependency($parameter));
$parameterType = $parameter->getType(); $parameterType = $parameter->getType();
// If the parameter is a built-in type, immediately skip reflection // If the parameter is a built-in type, immediately skip reflection
// stuff and attempt to give it a default or null value. // stuff and attempt to give it a default or null value.
if ($parameterType instanceof ReflectionNamedType && $parameterType->isBuiltin()) { if ($parameterType->isBuiltin()) {
return $this->autowireBuiltinDependency($parameter, $providedValue); 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. // Loop through each type until we hit a match.
foreach ($types as $type) { foreach ($parameter->getType()->split() as $type) {
try { try {
return $this->autowireObjectDependency($type, $providedValue); return $this->autowireObjectDependency(
type: $type,
tag: $tag,
providedValue: $providedValue
);
} catch (Throwable $throwable) { } catch (Throwable $throwable) {
// We were unable to resolve the dependency for the last union // We were unable to resolve the dependency for the last union
// type, so we are moving on to the next one. We hang onto // type, so we are moving on to the next one. We hang onto
@ -271,35 +306,29 @@ final class GenericContainer implements Container
// If the dependency has a default value, we do our best to prevent // If the dependency has a default value, we do our best to prevent
// an error by using that. // an error by using that.
if ($parameter->isDefaultValueAvailable()) { if ($parameter->hasDefaultValue()) {
return $parameter->getDefaultValue(); return $parameter->getDefaultValue();
} }
// At this point, there is nothing else we can do; we don't know // At this point, there is nothing else we can do; we don't know
// how to autowire this dependency. // how to autowire this dependency.
throw $lastThrowable ?? new CannotAutowireException($this->log); throw $lastThrowable ?? new CannotAutowireException($this->chain, new Dependency($parameter));
} }
private function autowireObjectDependency(ReflectionNamedType $type, mixed $providedValue): mixed private function autowireObjectDependency(TypeReflector $type, ?string $tag, mixed $providedValue): mixed
{ {
// If the provided value is of the right type, // If the provided value is of the right type,
// don't waste time autowiring, return it! // don't waste time autowiring, return it!
if (is_a($providedValue, $type->getName())) { if ($type->accepts($providedValue)) {
return $providedValue; return $providedValue;
} }
// If we can successfully retrieve an instance // If we can successfully retrieve an instance
// of the necessary dependency, return it. // of the necessary dependency, return it.
if ($instance = $this->resolve($type->getName())) { return $this->resolve(className: $type->getName(), tag: $tag);
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 private function autowireBuiltinDependency(ParameterReflector $parameter, mixed $providedValue): mixed
{ {
// Due to type coercion, the provided value may (or may not) work. // 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 // Here we give up trying to do type work for people. If they
@ -310,27 +339,59 @@ final class GenericContainer implements Container
// If the dependency has a default value, we might as well // If the dependency has a default value, we might as well
// use that at this point. // use that at this point.
if ($parameter->isDefaultValueAvailable()) { if ($parameter->hasDefaultValue()) {
return $parameter->getDefaultValue(); return $parameter->getDefaultValue();
} }
// If the dependency's type is an array or variadic variable, we'll // If the dependency's type is an array or variadic variable, we'll
// try to prevent an error by returning an empty array. // try to prevent an error by returning an empty array.
if ( if ($parameter->isVariadic() || $parameter->isIterable()) {
$parameter->getType()?->getName() === 'array' ||
$parameter->isVariadic()
) {
return []; return [];
} }
// If the dependency's type allows null or is optional, we'll // If the dependency's type allows null or is optional, we'll
// try to prevent an error by returning null. // try to prevent an error by returning null.
if ($parameter->allowsNull() || $parameter->isOptional()) { if (! $parameter->isRequired()) {
return null; return null;
} }
// At this point, there is nothing else we can do; we don't know // At this point, there is nothing else we can do; we don't know
// how to autowire this dependency. // how to autowire this dependency.
throw new CannotAutowireException($this->log); throw new CannotAutowireException($this->chain, new Dependency($parameter));
}
private function clone(): self
{
return clone $this;
}
private function resolveChain(): DependencyChain
{
if ($this->chain === null) {
$trace = debug_backtrace(DEBUG_BACKTRACE_IGNORE_ARGS);
$this->chain = new DependencyChain($trace[1]['file'] . ':' . $trace[1]['line']);
}
return $this->chain;
}
private function stopChain(): void
{
$this->chain = null;
}
public function __clone(): void
{
$this->chain = $this->chain?->clone();
}
private function resolveTaggedName(string|ClassReflector $class, ?string $tag): string
{
$className = is_string($class) ? $class : $class->getName();
return $tag
? "{$className}#{$tag}"
: $className;
} }
} }

View File

@ -2,7 +2,7 @@
declare(strict_types=1); declare(strict_types=1);
namespace PHPNative\Container\src; namespace PHPNative\Container;
trait HasInstance trait HasInstance
{ {

View File

@ -1,67 +0,0 @@
<?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;
}
}

View File

@ -2,7 +2,7 @@
declare(strict_types=1); declare(strict_types=1);
namespace PHPNative\Container\src; namespace PHPNative\Container;
interface Initializer interface Initializer
{ {

View File

@ -2,7 +2,7 @@
declare(strict_types=1); declare(strict_types=1);
namespace PHPNative\Container\src; namespace PHPNative\Container;
use Attribute; use Attribute;

View File

@ -0,0 +1,13 @@
<?php
namespace PHPNative\Container;
use Attribute;
#[Attribute]
final readonly class Tag
{
public function __construct(public string $name)
{
}
}

View File

@ -0,0 +1,112 @@
<?php
declare(strict_types=1);
namespace PHPNative\Core;
use ArrayIterator;
use IteratorAggregate;
use Traversable;
abstract class Collection implements IteratorAggregate
{
public function __construct(private array $elements)
{
}
public static function createEmpty(): static
{
return new static([]);
}
public static function fromMap(array $items, callable $fn): static
{
return new static(array_map($fn, $items));
}
public function reduce(callable $fn, mixed $initial): mixed
{
return array_reduce($this->elements, $fn, $initial);
}
public function map(callable $fn): array
{
return array_map($fn, $this->elements);
}
public function each(callable $fn): void
{
array_walk($this->elements, $fn);
}
public function some(callable $fn): bool
{
foreach ($this->elements as $index => $element) {
if ($fn($element, $index, $this->elements)) {
return true;
}
}
return false;
}
public function filter(callable $fn): static
{
return new static(array_filter($this->elements, $fn, ARRAY_FILTER_USE_BOTH));
}
public function first(): mixed
{
return reset($this->elements);
}
public function last(): mixed
{
return end($this->elements);
}
public function count(): int
{
return count($this->elements);
}
public function isEmpty(): bool
{
return empty($this->elements);
}
public function add(mixed $element): void
{
$this->elements[] = $element;
}
public function values(): array
{
return array_values($this->elements);
}
public function items(): array
{
return $this->elements;
}
public function getIterator(): Traversable
{
return new ArrayIterator($this->elements);
}
public function removeFirstItem(): void
{
if(count($this->elements) > 0) {
array_shift($this->elements);
}
}
public function removeItem($item): void
{
$temp = $this->filter(function ($value, $key) use($item){
return $value::class!=$item::class;
});
$this->elements = $temp->items();
}
}

View File

@ -0,0 +1,17 @@
<?php
namespace PHPNative\Core;
final readonly class PathHelper
{
public static function make(string ...$parts): string
{
$path = implode('/', $parts);
return str_replace(
['//', '\\'],
DIRECTORY_SEPARATOR,
$path,
);
}
}

View File

@ -0,0 +1,19 @@
<?php
declare(strict_types=1);
namespace PHPNative\Core;
abstract class TypedCollection extends Collection
{
public function __construct(array $elements = [])
{
parent::__construct($elements);
}
abstract protected function type(): string;
public function add(mixed $element): void
{
parent::add($element);
}
}

View File

@ -1,12 +0,0 @@
<?php
namespace PHPNative\Core;
class Window
{
public function __construct()
{
}
}

View File

@ -0,0 +1,17 @@
{
"name": "phpnative/event",
"license": "MIT",
"require": {
"php": "^8.3"
},
"autoload": {
"psr-4": {
"PHPNative\\Event\\": "src"
}
},
"autoload-dev": {
"psr-4": {
"PHPNative\\Event\\Tests\\": "tests"
}
}
}

View File

@ -0,0 +1,23 @@
<?php
declare(strict_types=1);
namespace PHPNative\Event;
class Driver
{
public function __construct(private \SDL_Event $event = new \SDL_Event())
{
}
public function pollEvent(): Event
{
\SDL_PollEvent($this->event);
return match($this->event->type) {
\SDL_EVENT_QUIT => new \PHPNative\Event\SystemEvent(EventType::QUIT),
\SDL_EVENT_MOUSE_BUTTON_DOWN => new MouseDown(EventType::MOUSEBUTTON_DOWN, $this->event->button->x, $this->event->button->y ),
\SDL_EVENT_MOUSE_BUTTON_UP => new MouseUp(EventType::MOUSEBUTTON_UP, $this->event->button->x, $this->event->button->y ),
\SDL_EVENT_MOUSE_MOTION => new MouseMove(EventType::MOUSEMOVE, $this->event->motion->x, $this->event->motion->y ),
default => new \PHPNative\Event\SystemEvent(EventType::NOOP)
};
}
}

View File

@ -0,0 +1,8 @@
<?php
namespace PHPNative\Event;
interface Event
{
public function getType(): EventType;
}

View File

@ -0,0 +1,14 @@
<?php
declare(strict_types=1);
namespace PHPNative\Event;
use PHPNative\Core\TypedCollection;
final class EventCollection extends TypedCollection
{
protected function type(): string
{
return Event::class;
}
}

View File

@ -0,0 +1,23 @@
<?php
namespace PHPNative\Event;
enum EventType: int
{
case NOOP = 0;
case QUIT = 1;
case CLOSE_WINDOW = 2;
case WINDOW_FOCUS_LOST = 1000;
case WINDOW_FOCUS_GAINED = 1001;
case WINDOW_RESIZED = 1002;
case WINDOW_CLOSE = 1003;
case MOUSEBUTTON_DOWN = 2000;
case MOUSEBUTTON_UP = 2001;
case MOUSEMOVE = 2002;
case TEXTINPUT = 3000;
case KEYUP = 3001;
case KEYDOWN = 3002;
}

View File

@ -0,0 +1,17 @@
<?php
declare(strict_types=1);
namespace PHPNative\Event;
class MouseDown implements Event
{
public function __construct(public EventType $type = EventType::MOUSEBUTTON_DOWN, public int $x = 0, public int $y = 0)
{
}
public function getType(): EventType
{
return $this->type;
}
}

View File

@ -0,0 +1,17 @@
<?php
declare(strict_types=1);
namespace PHPNative\Event;
class MouseMove implements Event
{
public function __construct(public EventType $type = EventType::MOUSEMOVE, public int $x = 0, public int $y = 0)
{
}
public function getType(): EventType
{
return $this->type;
}
}

View File

@ -0,0 +1,17 @@
<?php
declare(strict_types=1);
namespace PHPNative\Event;
class MouseUp implements Event
{
public function __construct(public EventType $type = EventType::MOUSEBUTTON_UP, public int $x = 0, public int $y = 0)
{
}
public function getType(): EventType
{
return $this->type;
}
}

View File

@ -0,0 +1,16 @@
<?php
declare(strict_types=1);
namespace PHPNative\Event;
class SystemEvent implements Event
{
public function __construct(public EventType $type = EventType::NOOP)
{
}
public function getType(): EventType
{
return $this->type;
}
}

View File

@ -6,9 +6,9 @@
}, },
"autoload": { "autoload": {
"psr-4": { "psr-4": {
"PHPNative\\Framework\\": "src" "PHPNative\\Framework\\": "src"
} }
}, },
"autoload-dev": { "autoload-dev": {
"psr-4": { "psr-4": {
"PHPNative\\Framework\\Tests\\": "tests" "PHPNative\\Framework\\Tests\\": "tests"

View File

@ -0,0 +1,10 @@
<?php
namespace PHPNative\Framework;
interface App
{
public function getName(): string;
public function getStartWindow(): string;
}

View File

@ -0,0 +1,10 @@
<?php
namespace PHPNative\Framework\Application;
interface Application
{
public function run(string|null $start): void;
}

View File

@ -0,0 +1,8 @@
<?php
namespace PHPNative\Framework\Application;
class Console
{
}

View File

@ -0,0 +1,52 @@
<?php
namespace PHPNative\Framework\Application;
use ArgumentCountError;
use PHPNative\Container\Container;
use PHPNative\Framework\App;
use PHPNative\Framework\Lifecycle\Lifecycle;
use PHPNative\Framework\PHPNative;
use Throwable;
final readonly class Gui implements Application
{
public function __construct(
private Container $container
) {
}
public static function boot(
string $name = 'PHPNative',
?string $root = null,
array $discoveryLocations = [],
): self {
$container = PHPNative::boot($root);
$application = $container->get(Gui::class);
return $application;
}
public function run(string|null $start): void
{
try {
$lifeCycle = $this->container->get(Lifecycle::class);
try {
$lifeCycle->show($this->container->get($start));
$lifeCycle->run();
} catch (ArgumentCountError $e) {
var_dump($e->getMessage());
var_dump($e->getFile());
var_dump($e->getTraceAsString());
}
} catch (Throwable $throwable) {
var_dump($throwable->getMessage());
var_dump($throwable->getFile());
var_dump($throwable->getTraceAsString());
}
}
}

View File

@ -1,47 +1,55 @@
<?PHP <?php
namespace PHPNative\Framework\src\Application; declare(strict_types=1);
use PHPNative\Container\src\Container; namespace PHPNative\Framework\Application;
use PHPNative\Container\src\GenericContainer;
use PHPNative\Framework\Application\ConfigBootstrap; use PHPNative\Container\Container;
use PHPNative\Framework\Application\DiscoveryBootstrap; use PHPNative\Container\GenericContainer;
use PHPNative\Framework\Application\DiscoveryLocationBootstrap; use PHPNative\Framework\Discovery\DiscoveryLocationBootstrap;
use PHPNative\Framework\Discovery\LoadDiscoveryClasses;
use PHPNative\Framework\Discovery\LoadDiscoveryLocations;
final class Kernel final class Kernel
{ {
public readonly Container $container;
private $loop; public array $discoveryClasses = [
];
public function __construct( public function __construct(
public string $root public string $root,
) public array $discoveryLocations = [],
{ ?Container $container = null,
) {
$this->container = $container ?? $this->createContainer();
$this
->registerKernel()
->loadDiscoveryLocations()
->loadDiscovery();
} }
public function run(): void private function registerKernel(): self
{ {
$this->init(); $this->container->singleton(self::class, $this);
return $this;
} }
public function init(): void private function loadDiscoveryLocations(): self
{ {
$container = $this->createContainer(); ($this->container->get(LoadDiscoveryLocations::class))();
$bootstraps = [ return $this;
DiscoveryLocationBootstrap::class, }
ConfigBootstrap::class,
DiscoveryBootstrap::class,
];
foreach ($bootstraps as $bootstrap) { private function loadDiscovery(): self
$container->get( {
$bootstrap, ($this->container->get(LoadDiscoveryClasses::class))();
kernel: $this,
)->boot();
}
return $container; return $this;
} }
private function createContainer(): Container private function createContainer(): Container
@ -50,11 +58,8 @@ final class Kernel
GenericContainer::setInstance($container); GenericContainer::setInstance($container);
$container $container->singleton(Container::class, fn () => $container);
->singleton(self::class, fn () => $this)
->singleton(Container::class, fn () => $container)
;
return $container; return $container;
} }
} }

View File

@ -1,10 +1,13 @@
<?php <?php
namespace PHPNative\Framework\src\Application; declare(strict_types=1);
namespace PHPNative\Framework\Application;
use PHPNative\UI\View; use PHPNative\UI\View;
interface Window interface Window
{ {
public function getTitle(): string;
public function getView(): View; public function getView(): View;
} }

View File

@ -0,0 +1,8 @@
<?php
namespace PHPNative\Framework\Discovery;
interface Bootstrap
{
public function boot(): void;
}

View File

@ -2,7 +2,7 @@
declare(strict_types=1); declare(strict_types=1);
namespace PHPNative\Framework\src\Discovery; namespace PHPNative\Framework\Discovery;
use PHPNative\Container\src\Container; use PHPNative\Container\src\Container;
use ReflectionClass; use ReflectionClass;

View File

@ -1,54 +0,0 @@
<?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);
}
}

View File

@ -2,7 +2,7 @@
declare(strict_types=1); declare(strict_types=1);
namespace PHPNative\Framework\src\Discovery; namespace PHPNative\Framework\Discovery;
final readonly class DiscoveryLocation final readonly class DiscoveryLocation
{ {

View File

@ -1,5 +1,7 @@
<?php <?php
declare(strict_types=1);
namespace PHPNative\Framework\src\Discovery; namespace PHPNative\Framework\src\Discovery;
use PHPNative\Container\src\Container; use PHPNative\Container\src\Container;
@ -8,7 +10,7 @@ use ReflectionClass;
final readonly class InitializerDiscovery implements Discovery final readonly class InitializerDiscovery implements Discovery
{ {
private const CACHE_PATH = __DIR__ . '/initializer-discovery.cache.php'; private const string CACHE_PATH = __DIR__ . '/initializer-discovery.cache.php';
public function __construct( public function __construct(
private Container $container, private Container $container,

View File

@ -0,0 +1,95 @@
<?php
declare(strict_types=1);
namespace PHPNative\Framework\Discovery;
use PHPNative\Container\Container;
use PHPNative\Framework\Application\Kernel;
final readonly class LoadDiscoveryClasses
{
public function __construct(
private Kernel $kernel,
private Container $container,
) {
}
public function __invoke(): void
{
reset($this->kernel->discoveryClasses);
while ($discoveryClass = current($this->kernel->discoveryClasses)) {
/** @var Discovery $discovery */
$discovery = $this->container->get($discoveryClass);
if ($this->kernel->discoveryCache && $discovery->hasCache()) {
$discovery->restoreCache($this->container);
next($this->kernel->discoveryClasses);
continue;
}
foreach ($this->kernel->discoveryLocations as $discoveryLocation) {
$directories = new RecursiveDirectoryIterator($discoveryLocation->path, FilesystemIterator::UNIX_PATHS | FilesystemIterator::SKIP_DOTS);
$files = new RecursiveIteratorIterator($directories);
/** @var SplFileInfo $file */
foreach ($files as $file) {
$fileName = $file->getFilename();
if ($fileName === '') {
continue;
}
if ($fileName === '.') {
continue;
}
if ($fileName === '..') {
continue;
}
$input = $file->getPathname();
if (ucfirst($fileName) === $fileName) {
// Trim ending slashing from path
$pathWithoutSlashes = rtrim($discoveryLocation->path, '\\/');
// Try to create a PSR-compliant class name from the path
$className = str_replace(
[
$pathWithoutSlashes,
'/',
'\\\\',
'.php',
],
[
$discoveryLocation->namespace,
'\\',
'\\',
'',
],
$file->getPathname(),
);
try {
$input = new ClassReflector($className);
} catch (Throwable) {
// Nothing should happen
}
}
if ($input instanceof ClassReflector) {
$discovery->discover($input);
} elseif ($discovery instanceof DiscoversPath) {
$discovery->discoverPath($input);
}
}
}
next($this->kernel->discoveryClasses);
$discovery->storeCache();
}
}
}

View File

@ -0,0 +1,122 @@
<?php
namespace PHPNative\Framework\Discovery;
use PHPNative\Core\PathHelper;
use PHPNative\Framework\Application\Kernel;
use PHPNative\Framework\Discovery\DiscoveryLocation;
final readonly class LoadDiscoveryLocations
{
public function __construct(
private Kernel $kernel,
) {
}
public function __invoke(): void
{
$this->kernel->discoveryLocations =
[
...$this->kernel->discoveryLocations,
...$this->discoverCorePackages(),
...$this->discoverAppNamespaces(),
...$this->discoverVendorPackages(),
];
}
/**
* @return DiscoveryLocation[]
*/
private function discoverCorePackages(): array
{
$composerPath = PathHelper::make($this->kernel->root, 'vendor/composer');
$installed = $this->loadJsonFile(PathHelper::make($composerPath, 'installed.json'));
$packages = $installed['packages'] ?? [];
$discoveredLocations = [];
foreach ($packages as $package) {
$packageName = ($package['name'] ?? '');
$isTempest = str_starts_with($packageName, 'tempest');
if (! $isTempest) {
continue;
}
$packagePath = PathHelper::make($composerPath, $package['install-path'] ?? '');
foreach ($package['autoload']['psr-4'] as $namespace => $namespacePath) {
$namespacePath = PathHelper::make($packagePath, $namespacePath);
$discoveredLocations[] = new DiscoveryLocation($namespace, $namespacePath);
}
}
return $discoveredLocations;
}
/**
* @return DiscoveryLocation[]
*/
private function discoverAppNamespaces(): array
{
$composer = $this->loadJsonFile(PathHelper::make($this->kernel->root, 'composer.json'));
$namespaceMap = $composer['autoload']['psr-4'] ?? [];
$discoveredLocations = [];
foreach ($namespaceMap as $namespace => $path) {
$path = PathHelper::make($this->kernel->root, $path);
$discoveredLocations[] = new DiscoveryLocation($namespace, $path);
}
return $discoveredLocations;
}
/**
* @return DiscoveryLocation[]
*/
private function discoverVendorPackages(): array
{
$composerPath = PathHelper::make($this->kernel->root, 'vendor/composer');
$installed = $this->loadJsonFile(PathHelper::make($composerPath, 'installed.json'));
$packages = $installed['packages'] ?? [];
$discoveredLocations = [];
foreach ($packages as $package) {
$packageName = ($package['name'] ?? '');
$isTempest = str_starts_with($packageName, 'tempest');
if ($isTempest) {
continue;
}
$packagePath = PathHelper::make($composerPath, $package['install-path'] ?? '');
$requiresTempest = isset($package['require']['tempest/framework']) || isset($package['require']['tempest/core']);
$hasPsr4Namespaces = isset($package['autoload']['psr-4']);
if (! ($requiresTempest && $hasPsr4Namespaces)) {
continue;
}
foreach ($package['autoload']['psr-4'] as $namespace => $namespacePath) {
$path = PathHelper::make($packagePath, $namespacePath);
$discoveredLocations[] = new DiscoveryLocation($namespace, $path);
}
}
return $discoveredLocations;
}
private function loadJsonFile(string $path): array
{
if (! file_exists($path)) {
throw new DiscoveryException(sprintf('Could not locate %s, try running "composer install"', $path));
}
return json_decode(file_get_contents($path), true);
}
}

View File

@ -0,0 +1,42 @@
<?php
namespace PHPNative\Framework\Lifecycle;
use PHPNative\Event\Event;
use PHPNative\Framework\Application\Window;
use PHPNative\Renderer\Thread;
class Context
{
public function __construct(private Thread $thread)
{
}
public function show(Window $window): void
{
$this->thread->show($window);
}
public function update(float $delta): void
{
}
public function event(Event $event): void
{
$this->thread->addEvent($event);
}
public function render(float $delta): void
{
$this->thread->render();
}
public function unload(): void
{
//TODO: unload
}
}

View File

@ -0,0 +1,15 @@
<?php
namespace PHPNative\Framework\Lifecycle;
use PHPNative\Core\TypedCollection;
#[Singleton]
class ContextCollection extends TypedCollection
{
protected function type(): string
{
return Context::class;
}
}

View File

@ -0,0 +1,87 @@
<?php
namespace PHPNative\Framework\Lifecycle;
use PHPNative\Container\Container;
use PHPNative\Event\Event;
use PHPNative\Event\EventType;
use PHPNative\Framework\Application\Window;
use PHPNative\Framework\Loop\OrderedEventLoop;
use PHPNative\Framework\Loop\WorkerInterface;
use PHPNative\Renderer\Thread;
class Lifecycle implements WorkerInterface
{
public function __construct(
private OrderedEventLoop $loop,
private ContextCollection $contextCollection,
private Container $container)
{
$this->loop->use($this);
}
public function show(Window $window, array $arguments = []): void
{
$context = $this->container->get(
Context::class,
thread: $this->container->get(Thread::class)
);
$context->show($window);
$this->contextCollection->add($context);
}
public function run(): void
{
$this->loop->run();
}
public function onUpdate(float $delta): void
{
$this->contextCollection->map(fn(Context $context) => $context->update($delta));
}
public function onRender(float $delta): void
{
$this->contextCollection->map(fn(Context $context) => $context->render($delta));
}
public function onEvent( $event): void
{
$this->defaultEventLogic($event);
$this->contextCollection->map(fn(Context $context) => $context->event($event));
}
protected function defaultEventLogic(Event $event): void
{
switch ($event->getType()) {
case EventType::WINDOW_FOCUS_LOST:
$this->loop->pause();
break;
case EventType::WINDOW_FOCUS_GAINED:
$this->loop->resume();
break;
case EventType::QUIT:
$this->contextCollection->map(fn(Context $context) => $context->unload());
$this->contextCollection = new ContextCollection();
$this->loop->stop();
break;
}
}
public function onPause(): void
{
if ($this->context !== null) {
$this->context->pause();
}
}
public function onResume(): void
{
if ($this->context !== null) {
$this->context->resume();
}
}
}

View File

@ -0,0 +1,82 @@
<?php
namespace PHPNative\Framework\Loop;
use PHPNative\Event\Event;
abstract class EventLoop implements LoopInterface
{
protected bool $running = false;
protected bool $paused = false;
private ?WorkerInterface $worker = null;
public function use(?WorkerInterface $worker): void
{
$this->worker = $worker;
}
public function run(int $frameRate = self::DEFAULT_FRAME_RATE, int $updateRate = self::DEFAULT_UPDATE_RATE): void
{
$this->paused = false;
if ($this->running) {
return;
}
try {
$this->running = true;
$this->execute($frameRate, $updateRate);
} finally {
}
}
abstract protected function execute(int $frameRate, int $updateRate): void;
public function pause(): void
{
if ($this->paused === false && $this->worker !== null) {
$this->worker->onPause();
}
$this->paused = true;
}
public function resume(): void
{
if ($this->paused === true && $this->worker !== null) {
$this->worker->onResume();
}
$this->paused = false;
}
public function stop(): void
{
$this->running = false;
}
protected function render(float $delta): void
{
if ($this->worker !== null) {
$this->worker->onRender($delta);
}
}
protected function update(float $delta): void
{
if ($this->worker !== null && $this->paused === false) {
$this->worker->onUpdate($delta);
}
}
protected function poll(Event $event): void
{
if ($this->worker !== null && $this->paused === false) {
$this->worker->onEvent($event);
}
}
}

View File

@ -0,0 +1,18 @@
<?php
namespace PHPNative\Framework\Loop;
interface LoopInterface
{
public const DEFAULT_FRAME_RATE = 60;
public const DEFAULT_UPDATE_RATE = 60;
public function run(int $frameRate = self::DEFAULT_FRAME_RATE, int $updateRate = self::DEFAULT_UPDATE_RATE): void;
public function pause(): void;
public function resume(): void;
public function stop(): void;
}

View File

@ -0,0 +1,42 @@
<?php
namespace PHPNative\Framework\Loop;
use PHPNative\Event\Driver;
use PHPNative\Event\EventType;
class OrderedEventLoop extends EventLoop
{
public Timer $render;
public Timer $updates;
public function __construct(private Driver $eventDriver)
{
$this->render = new Timer(self::DEFAULT_FRAME_RATE);
$this->updates = new Timer(self::DEFAULT_UPDATE_RATE);
}
protected function execute(int $frameRate, int $updateRate): void
{
$this->render->rate($frameRate)->touch();
$this->updates->rate($updateRate)->touch();
while ($this->running) {
$now = \microtime(true);
if (($delta = $this->updates->next($now)) !== null) {
$this->update($delta);
}
if (($delta = $this->render->next($now)) !== null) {
$this->render($delta);
}
while ($event = $this->eventDriver->pollEvent()) {
if($event->getType() == EventType::NOOP) break;
$this->poll($event);
}
}
}
}

View File

@ -1,7 +1,10 @@
<?php <?php
declare(strict_types=1); declare(strict_types=1);
namespace PHPNative\Framework\src\Loop; namespace PHPNative\Framework\Loop;
use function microtime;
class Timer class Timer
{ {
@ -20,10 +23,9 @@ class Timer
public function touch(float $now = null): void public function touch(float $now = null): void
{ {
$this->time = $now ?? \microtime(true); $this->time = $now ?? microtime(true);
} }
public function rate(int $rate): self public function rate(int $rate): self
{ {
$this->rate = $rate === 0 ? 0 : 1 / $rate; $this->rate = $rate === 0 ? 0 : 1 / $rate;

View File

@ -0,0 +1,14 @@
<?php
namespace PHPNative\Framework\Loop;
interface WorkerInterface
{
public function onUpdate(float $delta): void;
public function onRender(float $delta): void;
public function onPause(): void;
public function onResume(): void;
}

View File

@ -1,16 +1,24 @@
<?PHP <?php
namespace PHPNative\Framework\src; declare(strict_types=1);
use PHPNative\Framework\src\Application\Kernel; namespace PHPNative\Framework;
final class PHPNative { use PHPNative\Container\Container;
use PHPNative\Framework\Application\Kernel;
public static function boot(string $directory): Kernel final class PHPNative
{ {
public static function boot(
return new Kernel($directory); ?string $root = null,
array $discoveryLocations = [],
): Container {
$root ??= getcwd();
// Kernel
return (new Kernel(
root: $root,
discoveryLocations: $discoveryLocations,
))->container;
} }
} }

View File

@ -0,0 +1,17 @@
{
"name": "phpnative/renderer",
"license": "MIT",
"require": {
"php": "^8.3"
},
"autoload": {
"psr-4": {
"PHPNative\\Renderer\\": "src"
}
},
"autoload-dev": {
"psr-4": {
"PHPNative\\Renderer\\Tests\\": "tests"
}
}
}

View File

@ -0,0 +1,84 @@
<?php
namespace PHPNative\Renderer;
use PHPNative\Event\Event;
use PHPNative\Event\EventCollection;
use PHPNative\Framework\Application\Window;
use PHPNative\Tailwind\Style\Background;
use PHPNative\Tailwind\Style\MediaQueryEnum;
use PHPNative\Tailwind\Style\Padding;
use PHPNative\Tailwind\StyleParser;
class Thread
{
private ?\SDL_Window $windowId = null;
private $rendererPtr = null;
private Window $window;
private EventCollection $eventStack;
public function __construct()
{
$this->eventStack = new EventCollection();
}
public function show(Window $window): void
{
$this->window = $window;
\SDL_Init(SDL_INIT_VIDEO);
\SDL_TTF_Init();
$this->windowId = \SDL_CreateWindow($this->window->getTitle(), 800, 600, \SDL_WINDOW_HIGH_PIXEL_DENSITY);
$this->rendererPtr = \SDL_CreateRenderer($this->windowId);
}
public function close(): void
{
\SDL_DestroyRenderer($this->rendererPtr);
\SDL_DestroyWindow($this->windowId);
\SDL_Quit();
}
public function render(): void
{
$windowWidth = 0;
$windowHeight = 0;
\SDL_GetWindowSize($this->windowId, $windowWidth, $windowHeight);
$viewPort = new Viewport($this->windowId, $this->rendererPtr, 0, 0, $windowWidth, $windowHeight, $windowWidth, $windowHeight, MediaQueryEnum::getFromPixel($windowWidth));
$this->startRender();
Widget::render($this, $viewPort, $this->window->getView());
$this->endRender();
$this->eventStack->removeFirstItem();
}
private function startRender(): void
{
}
private function endRender(): void
{
\SDL_RenderPresent($this->rendererPtr);
}
public function addEvent(Event $event): void
{
$this->eventStack->removeItem($event);
$this->eventStack->add($event);
}
public function getEvent(): ?Event
{
if($this->eventStack->count() > 0) {
return $this->eventStack->first();
}
return null;
}
}

View File

@ -0,0 +1,25 @@
<?php
declare(strict_types=1);
namespace PHPNative\Renderer;
use PHPNative\Tailwind\Style\MediaQueryEnum;
class Viewport
{
public function __construct(public $windowId,
public $renderPtr,
public int $x = 0,
public int $y = 0,
public $width = 0,
public $height = 0,
public $windowWidth = 0,
public $windowHeight = 0,
public MediaQueryEnum $windowMediaQuery = MediaQueryEnum::normal)
{
}
}

View File

@ -0,0 +1,26 @@
<?php
namespace PHPNative\Renderer;
use PHPNative\UI\BaseView;
use PHPNative\UI\View;
use PHPNative\UI\Widget\Button;
use PHPNative\UI\Widget\Container;
class Widget
{
public static function render(Thread $thread, ViewPort $viewPort, View $view): Viewport {
if($view instanceof BaseView) {
return \PHPNative\Renderer\Widgets\BaseView::render($thread, $viewPort, $view);
}
if($view instanceof Button) {
return \PHPNative\Renderer\Widgets\Button::render($thread, $viewPort, $view);
}
if($view instanceof Container) {
return \PHPNative\Renderer\Widgets\Container::render($thread, $viewPort, $view);
}
}
}

View File

@ -0,0 +1,47 @@
<?php
namespace PHPNative\Renderer\Widgets;
use PHPNative\Renderer\Thread;
use PHPNative\Renderer\Viewport;
use PHPNative\Renderer\Widget;
use PHPNative\Tailwind\Style\Background;
use PHPNative\Tailwind\Style\Margin;
use PHPNative\Tailwind\Style\MediaQueryEnum;
use PHPNative\Tailwind\Style\Padding;
use PHPNative\Tailwind\Style\StateEnum;
use PHPNative\Tailwind\StyleParser;
class BaseView
{
public static function render(Thread $thread, Viewport $viewport, \PHPNative\UI\BaseView $view): Viewport
{
$styles = StyleParser::parse($view->style)->getValidStyles($viewport->windowMediaQuery, StateEnum::normal);
if(isset($styles[Margin::class]) && $m = $styles[Margin::class]) {
$viewport->x += $m->left;
$viewport->width -= ($m->right + $m->left);
$viewport->y += $m->top;
$viewport->height -= ($m->bottom + $m->top);
}
if(isset($styles[Background::class]) && $bg = $styles[Background::class]) {
$rect = new \SDL_FRect($viewport->x, $viewport->y, $viewport->width, $viewport->height);
\SDL_SetRenderDrawColor($viewport->renderPtr, $bg->color->red, $bg->color->green, $bg->color->blue, $bg->color->alpha);
\SDL_RenderFillRect($viewport->renderPtr, $rect);
}
if(isset($styles[Padding::class]) && $m = $styles[Padding::class]) {
$viewport->x += $m->left;
$viewport->width -= ($m->right + $m->left);
$viewport->y += $m->top;
$viewport->height -= ($m->bottom + $m->top);
}
if($view->getView() !== null) {
Widget::render($thread, $viewport, $view->getView());
}
return $viewport;
}
}

View File

@ -0,0 +1,90 @@
<?php
namespace PHPNative\Renderer\Widgets;
use PHPNative\Event\EventType;
use PHPNative\Renderer\Thread;
use PHPNative\Renderer\Viewport;
use PHPNative\Renderer\Widget;
use PHPNative\Tailwind\Style\Background;
use PHPNative\Tailwind\Style\Margin;
use PHPNative\Tailwind\Style\MediaQueryEnum;
use PHPNative\Tailwind\Style\Padding;
use PHPNative\Tailwind\Style\StateEnum;
use PHPNative\Tailwind\Style\Unit;
use PHPNative\Tailwind\Style\Width;
use PHPNative\Tailwind\StyleParser;
class Button
{
public static function render(Thread $thread, Viewport $viewport, \PHPNative\UI\Widget\Button $view): Viewport
{
$styles = StyleParser::parse($view->style)->getValidStyles($viewport->windowMediaQuery, $view->state);
$font = \SDL_TTF_OpenFont(__DIR__ . DIRECTORY_SEPARATOR . '../../../../../assets/segoe-ui.ttf' , 30);
$color = new \SDL_Color(0,0,0,0);
$surface = \SDL_TTF_RenderText_Blended($font, $view->label, $color);
$texture = \SDL_CreateTextureFromSurface($viewport->renderPtr, $surface);
if(isset($styles[Width::class]) && $m = $styles[Width::class]) {
if($styles[Width::class]->unit === Unit::Pixel) {
$viewport->width = $styles[Width::class]->value;
}elseif($styles[Width::class]->unit === Unit::Percent) {
$viewport->width = $viewport->width/100*$styles[Width::class]->value;
}
}else{
$viewport->width = $surface->w;
$viewport->height = $surface->h;
}
if(isset($styles[Margin::class]) && $m = $styles[Margin::class]) {
$viewport->x += $m->left;
$viewport->y += $m->top;
$viewport->width += $m->right;
$viewport->height += $m->bottom;
}
$bgX = $viewport->x;
$bgY = $viewport->y;
if(isset($styles[Padding::class]) && $p = $styles[Padding::class]) {
$viewport->x += $p->left;
$viewport->width += ($p->right + $p->left);
$viewport->y += $p->top;
$viewport->height += ($p->bottom + $p->top);
}
if(isset($styles[Background::class]) && $bg = $styles[Background::class]) {
$rect = new \SDL_FRect($bgX, $bgY, $viewport->width, $viewport->height);
\SDL_SetRenderDrawColor($viewport->renderPtr, $bg->color->red, $bg->color->green, $bg->color->blue, $bg->color->alpha);
\SDL_RenderFillRect($viewport->renderPtr, $rect);
}
if($thread->getEvent() && $thread->getEvent()->getType() === EventType::MOUSEMOVE) {
if( $viewport->x <= $thread->getEvent()->x &&
$thread->getEvent()->x <= $viewport->x + $viewport->width &&
$viewport->y <= $thread->getEvent()->y &&
$thread->getEvent()->y <= $viewport->y + $viewport->height ) {
$view->state = StateEnum::hover;
}else{
$view->state = StateEnum::normal;
}
}
if($thread->getEvent() && $thread->getEvent()->getType() === EventType::MOUSEBUTTON_UP) {
if( $viewport->x <= $thread->getEvent()->x && $thread->getEvent()->x <= $viewport->x + $viewport->width && $viewport->y <= $thread->getEvent()->y && $thread->getEvent()->y <= $viewport->y + $viewport->height ) {
$view->onClick();
}
}
$rect = new \SDL_FRect($viewport->x,$viewport->y,$surface->w,$surface->h);
\SDL_RenderTexture($viewport->renderPtr, $texture, null, $rect);
return $viewport;
}
}

View File

@ -0,0 +1,46 @@
<?php
namespace PHPNative\Renderer\Widgets;
use PHPNative\Renderer\Thread;
use PHPNative\Renderer\Viewport;
use PHPNative\Renderer\Widget;
use PHPNative\Tailwind\Style\Background;
use PHPNative\Tailwind\Style\Margin;
use PHPNative\Tailwind\Style\Padding;
use PHPNative\Tailwind\Style\StateEnum;
use PHPNative\Tailwind\StyleParser;
class Container
{
public static function render(Thread $thread, Viewport $viewport, \PHPNative\UI\Widget\Container $view): Viewport
{
$styles = StyleParser::parse($view->style)->getValidStyles($viewport->windowMediaQuery, StateEnum::normal);
if(isset($styles[Margin::class]) && $m = $styles[Margin::class]) {
$viewport->x += $m->left;
$viewport->width -= ($m->right + $m->left);
$viewport->y += $m->top;
$viewport->height -= ($m->bottom + $m->top);
}
if(isset($styles[Background::class]) && $bg = $styles[Background::class]) {
$rect = new \SDL_FRect($viewport->x, $viewport->y, $viewport->width, $viewport->height);
\SDL_SetRenderDrawColor($viewport->renderPtr, $bg->color->red, $bg->color->green, $bg->color->blue, $bg->color->alpha);
\SDL_RenderFillRect($viewport->renderPtr, $rect);
}
if(isset($styles[Padding::class]) && $m = $styles[Padding::class]) {
$viewport->x += $m->left;
$viewport->width -= ($m->right + $m->left);
$viewport->y += $m->top;
$viewport->height -= ($m->bottom + $m->top);
}
foreach($view->subViews as $subView) {
Widget::render($thread, clone $viewport, $subView);
}
return $viewport;
}
}

View File

@ -0,0 +1,17 @@
{
"name": "phpnative/support",
"license": "MIT",
"require": {
"php": "^8.3"
},
"autoload": {
"psr-4": {
"PHPNative\\Support\\": "src"
}
},
"autoload-dev": {
"psr-4": {
"PHPNative\\Support\\Tests\\": "tests"
}
}
}

View File

@ -0,0 +1,126 @@
<?php
declare(strict_types=1);
namespace PHPNative\Support\Reflection;
use Generator;
use ReflectionClass as PHPReflectionClass;
use ReflectionMethod as PHPReflectionMethod;
use ReflectionProperty as PHPReflectionProperty;
/**
* @template TClassName
*/
final readonly class ClassReflector implements Reflector
{
use HasAttributes;
private PHPReflectionClass $reflectionClass;
/**
* @param class-string<TClassName>|object<TClassName>|PHPReflectionClass<TClassName> $reflectionClass
* @phpstan-ignore-next-line
*/
public function __construct(string|object $reflectionClass)
{
if (is_string($reflectionClass)) {
$reflectionClass = new PHPReflectionClass($reflectionClass);
} elseif (! $reflectionClass instanceof PHPReflectionClass && is_object($reflectionClass)) {
$reflectionClass = new PHPReflectionClass($reflectionClass);
}
$this->reflectionClass = $reflectionClass;
}
public function getReflection(): PHPReflectionClass
{
return $this->reflectionClass;
}
/** @return Generator<PropertyReflector> */
public function getPublicProperties(): Generator
{
foreach ($this->reflectionClass->getProperties(PHPReflectionProperty::IS_PUBLIC) as $property) {
yield new PropertyReflector($property);
}
}
/** @return Generator<MethodReflector> */
public function getPublicMethods(): Generator
{
foreach ($this->reflectionClass->getMethods(PHPReflectionMethod::IS_PUBLIC) as $method) {
yield new MethodReflector($method);
}
}
public function getProperty(string $name): PropertyReflector
{
return new PropertyReflector(new PHPReflectionProperty($this->reflectionClass->getName(), $name));
}
/**
* @return class-string<TClassName>
*/
public function getName(): string
{
return $this->reflectionClass->getName();
}
public function getShortName(): string
{
return $this->reflectionClass->getShortName();
}
public function getType(): TypeReflector
{
return new TypeReflector($this->reflectionClass);
}
public function getConstructor(): ?MethodReflector
{
$constructor = $this->reflectionClass->getConstructor();
if ($constructor === null) {
return null;
}
return new MethodReflector($constructor);
}
public function getMethod(string $name): ?MethodReflector
{
return new MethodReflector($this->reflectionClass->getMethod($name));
}
public function isInstantiable(): bool
{
return $this->reflectionClass->isInstantiable();
}
public function newInstanceWithoutConstructor(): object
{
return $this->reflectionClass->newInstanceWithoutConstructor();
}
public function newInstanceArgs(array $args = []): object
{
return $this->reflectionClass->newInstanceArgs($args);
}
public function callStatic(string $method, mixed ...$args): mixed
{
$className = $this->getName();
return $className::$method(...$args);
}
public function is(string $className): bool
{
return $this->getType()->matches($className);
}
public function implements(string $interface): bool
{
return $this->isInstantiable() && $this->getType()->matches($interface);
}
}

View File

@ -0,0 +1,50 @@
<?php
declare(strict_types=1);
namespace PHPNative\Support\Reflection;
use Closure;
use Generator;
use ReflectionFunction as PHPReflectionFunction;
final readonly class FunctionReflector implements Reflector
{
private PHPReflectionFunction $reflectionFunction;
public function __construct(
PHPReflectionFunction|Closure $function
) {
$this->reflectionFunction = $function instanceof Closure
? new PHPReflectionFunction($function)
: $function;
}
/** @return Generator|\PHPNative\Support\Reflection\ParameterReflector[] */
public function getParameters(): Generator
{
foreach ($this->reflectionFunction->getParameters() as $parameter) {
yield new ParameterReflector($parameter);
}
}
public function getName(): string
{
return $this->reflectionFunction->getName();
}
public function getShortName(): string
{
return $this->reflectionFunction->getShortName();
}
public function getFileName(): string
{
return $this->reflectionFunction->getFileName();
}
public function getStartLine(): int
{
return (int) $this->reflectionFunction->getStartLine();
}
}

View File

@ -0,0 +1,46 @@
<?php
declare(strict_types=1);
namespace PHPNative\Support\Reflection;
use ReflectionAttribute as PHPReflectionAttribute;
use ReflectionClass as PHPReflectionClass;
use ReflectionMethod as PHPReflectionMethod;
use ReflectionParameter as PHPReflectionParameter;
use ReflectionProperty as PHPReflectionProperty;
trait HasAttributes
{
abstract public function getReflection(): PHPReflectionClass|PHPReflectionMethod|PHPReflectionProperty|PHPReflectionParameter;
public function hasAttribute(string $name): bool
{
return $this->getReflection()->getAttributes($name) !== [];
}
/**
* @template TAttributeClass of object
* @param class-string<TAttributeClass> $attributeClass
* @return TAttributeClass|null
*/
public function getAttribute(string $attributeClass): object|null
{
$attribute = $this->getReflection()->getAttributes($attributeClass, PHPReflectionAttribute::IS_INSTANCEOF)[0] ?? null;
return $attribute?->newInstance();
}
/**
* @template TAttributeClass of object
* @param class-string<TAttributeClass> $attributeClass
* @return TAttributeClass[]
*/
public function getAttributes(string $attributeClass): array
{
return array_map(
fn (PHPReflectionAttribute $attribute) => $attribute->newInstance(),
$this->getReflection()->getAttributes($attributeClass, PHPReflectionAttribute::IS_INSTANCEOF)
);
}
}

View File

@ -0,0 +1,87 @@
<?php
declare(strict_types=1);
namespace PHPNative\Support\Reflection;
use Generator;
use ReflectionMethod as PHPReflectionMethod;
final readonly class MethodReflector implements Reflector
{
use HasAttributes;
public function __construct(
private PHPReflectionMethod $reflectionMethod,
) {
}
public static function fromParts(string|object $class, string $name): self
{
return new self(new PHPReflectionMethod($class, $name));
}
public function getReflection(): PHPReflectionMethod
{
return $this->reflectionMethod;
}
/** @return Generator|\Tempest\Support\Reflection\ParameterReflector[] */
public function getParameters(): Generator
{
foreach ($this->reflectionMethod->getParameters() as $parameter) {
yield new ParameterReflector($parameter);
}
}
public function invokeArgs(object|null $object, array $args = []): mixed
{
return $this->reflectionMethod->invokeArgs($object, $args);
}
public function getReturnType(): TypeReflector
{
return new TypeReflector($this->reflectionMethod->getReturnType());
}
public function getDeclaringClass(): ClassReflector
{
return new ClassReflector($this->reflectionMethod->getDeclaringClass());
}
public function getName(): string
{
return $this->reflectionMethod->getName();
}
public function getShortName(): string
{
$string = $this->getDeclaringClass()->getShortName() . '::' . $this->getName() . '(';
$parameters = [];
foreach ($this->getParameters() as $parameter) {
$parameters[] = $parameter->getType()->getShortName() . ' $' . $parameter->getName();
}
$string .= implode(', ', $parameters);
return $string . ')';
}
public function __serialize(): array
{
return [
'class' => $this->reflectionMethod->getDeclaringClass()->getName(),
'method' => $this->reflectionMethod->getName(),
];
}
public function __unserialize(array $data): void
{
$this->reflectionMethod = new PHPReflectionMethod(
objectOrMethod: $data['class'],
method: $data['method'],
);
}
}

View File

@ -0,0 +1,73 @@
<?php
declare(strict_types=1);
namespace PHPNative\Support\Reflection;
use ReflectionParameter as PHPReflectionParameter;
final readonly class ParameterReflector implements Reflector
{
use HasAttributes;
public function __construct(
private PHPReflectionParameter $reflectionParameter,
) {
}
public function getReflection(): PHPReflectionParameter
{
return $this->reflectionParameter;
}
public function getName(): string
{
return $this->reflectionParameter->getName();
}
public function getType(): TypeReflector
{
return new TypeReflector($this->reflectionParameter);
}
public function isOptional(): bool
{
return $this->reflectionParameter->isOptional();
}
public function isDefaultValueAvailable(): bool
{
return $this->reflectionParameter->isDefaultValueAvailable();
}
public function getPosition(): int
{
return $this->reflectionParameter->getPosition();
}
public function hasDefaultValue(): bool
{
return $this->reflectionParameter->isDefaultValueAvailable();
}
public function getDefaultValue(): mixed
{
return $this->reflectionParameter->getDefaultValue();
}
public function isVariadic(): bool
{
return $this->reflectionParameter->isVariadic();
}
public function isIterable(): bool
{
return $this->getType()->isIterable();
}
public function isRequired(): bool
{
return ! $this->reflectionParameter->allowsNull()
&& ! $this->reflectionParameter->isOptional();
}
}

View File

@ -0,0 +1,130 @@
<?php
declare(strict_types=1);
namespace PHPNative\Support\Reflection;
use Error;
use ReflectionProperty as PHPReflectionProperty;
final readonly class PropertyReflector implements Reflector
{
use HasAttributes;
public function __construct(
private PHPReflectionProperty $reflectionProperty,
) {
}
public static function fromParts(string|object $class, string $name): self
{
return new self(new PHPReflectionProperty($class, $name));
}
public function getReflection(): PHPReflectionProperty
{
return $this->reflectionProperty;
}
public function getValue(object $object): mixed
{
return $this->reflectionProperty->getValue($object);
}
public function setValue(object $object, mixed $value): void
{
$this->reflectionProperty->setValue($object, $value);
}
public function isInitialized(object $object): bool
{
return $this->reflectionProperty->isInitialized($object);
}
public function accepts(mixed $input): bool
{
return $this->getType()->accepts($input);
}
public function getClass(): ClassReflector
{
return new ClassReflector($this->reflectionProperty->getDeclaringClass());
}
public function getType(): ?TypeReflector
{
return new TypeReflector($this->reflectionProperty);
}
public function isIterable(): bool
{
return $this->getType()->isIterable();
}
public function isPromoted(): bool
{
return $this->reflectionProperty->isPromoted();
}
public function getIterableType(): ?TypeReflector
{
$doc = $this->reflectionProperty->getDocComment();
if (! $doc) {
return null;
}
preg_match('/@var ([\\\\\w]+)\[]/', $doc, $match);
if (! isset($match[1])) {
return null;
}
return new TypeReflector(ltrim($match[1], '\\'));
}
public function isUninitialized(object $object): bool
{
return ! $this->reflectionProperty->isInitialized($object);
}
public function unset(object $object): void
{
unset($object->{$this->getName()});
}
public function set(object $object, mixed $value): void
{
$this->reflectionProperty->setValue($object, $value);
}
public function get(object $object, mixed $default = null): mixed
{
try {
return $this->reflectionProperty->getValue($object) ?? $default;
} catch (Error $error) {
return $default ?? throw $error;
}
}
public function getName(): string
{
return $this->reflectionProperty->getName();
}
public function hasDefaultValue(): bool
{
$constructorParameters = [];
foreach (($this->getClass()->getConstructor()?->getParameters() ?? []) as $parameter) {
$constructorParameters[$parameter->getName()] = $parameter;
}
$hasDefaultValue = $this->reflectionProperty->hasDefaultValue();
$hasPromotedDefaultValue = $this->isPromoted()
&& $constructorParameters[$this->getName()]->isDefaultValueAvailable();
return $hasDefaultValue || $hasPromotedDefaultValue;
}
}

View File

@ -0,0 +1,10 @@
<?php
declare(strict_types=1);
namespace PHPNative\Support\Reflection;
interface Reflector
{
public function getName(): string;
}

View File

@ -0,0 +1,152 @@
<?php
declare(strict_types=1);
namespace PHPNative\Support\Reflection;
use Exception;
use Generator;
use ReflectionClass as PHPReflectionClass;
use ReflectionIntersectionType as PHPReflectionIntersectionType;
use ReflectionNamedType as PHPReflectionNamedType;
use ReflectionParameter as PHPReflectionParameter;
use ReflectionProperty as PHPReflectionProperty;
use ReflectionType as PHPReflectionType;
use ReflectionUnionType as PHPReflectionUnionType;
use Reflector as PHPReflector;
use TypeError;
final readonly class TypeReflector implements Reflector
{
private string $definition;
public function __construct(
private PHPReflector|PHPReflectionType|string $reflector,
) {
$this->definition = $this->resolveDefinition($this->reflector);
}
public function asClass(): ClassReflector
{
return new ClassReflector($this->definition);
}
public function equals(string|TypeReflector $type): bool
{
if (is_string($type)) {
$type = new TypeReflector($type);
}
return $this->definition === $type->definition;
}
public function accepts(mixed $input): bool
{
$test = eval(sprintf('return fn (%s $input) => $input;', $this->definition));
try {
$test($input);
} catch (TypeError) {
return false;
}
return true;
}
public function matches(string $className): bool
{
return is_a($this->definition, $className, true);
}
public function getName(): string
{
return $this->definition;
}
public function getShortName(): string
{
$parts = explode('\\', $this->definition);
return $parts[array_key_last($parts)];
}
public function isBuiltIn(): bool
{
return in_array($this->definition, [
'string',
'bool',
'float',
'int',
'array',
'null',
'object',
'callable',
'resource',
'never',
'void',
'true',
'false',
]);
}
public function isClass(): bool
{
return class_exists($this->definition);
}
public function isIterable(): bool
{
return in_array($this->definition, [
'array',
'iterable',
Generator::class,
]);
}
/** @return self[] */
public function split(): array
{
return array_map(
fn (string $part) => new self($part),
preg_split('/[&|]/', $this->definition),
);
}
private function resolveDefinition(PHPReflector|PHPReflectionType|string $reflector): string
{
if (is_string($reflector)) {
return $reflector;
}
if (
$reflector instanceof PHPReflectionParameter
|| $reflector instanceof PHPReflectionProperty
) {
return $this->resolveDefinition($reflector->getType());
}
if ($reflector instanceof PHPReflectionClass) {
return $reflector->getName();
}
if ($reflector instanceof PHPReflectionNamedType) {
return $reflector->getName();
}
if ($reflector instanceof PHPReflectionUnionType) {
return implode('|', array_map(
fn (PHPReflectionType $reflectionType) => $this->resolveDefinition($reflectionType),
$reflector->getTypes(),
));
}
if ($reflector instanceof PHPReflectionIntersectionType) {
return implode('&', array_map(
fn (PHPReflectionType $reflectionType) => $this->resolveDefinition($reflectionType),
$reflector->getTypes(),
));
}
throw new Exception('Could not resolve type');
}
}

View File

@ -0,0 +1,17 @@
<?php
namespace PHPNative\Tailwind\Model;
use PHPNative\Tailwind\Style\MediaQueryEnum;
use PHPNative\Tailwind\Style\StateEnum;
class Style
{
public function __construct(
public \PHPNative\Tailwind\Style\Style $style,
public MediaQueryEnum $mediaQuery = MediaQueryEnum::normal,
public StateEnum $state = StateEnum::normal
)
{
}
}

View File

@ -0,0 +1,47 @@
<?php
namespace PHPNative\Tailwind\Model;
use PHPNative\Core\TypedCollection;
use PHPNative\Tailwind\Style\Margin;
use PHPNative\Tailwind\Style\MediaQueryEnum;
use PHPNative\Tailwind\Style\Padding;
use PHPNative\Tailwind\Style\StateEnum;
class StyleCollection extends TypedCollection
{
protected function type(): string
{
return Style::class;
}
public function getValidStyles(MediaQueryEnum $mediaQueryEnum, StateEnum $state): array
{
$items = [];
foreach($this->items() as $style) {
if(($style->state == StateEnum::normal || $style->state === $state) && ($style->mediaQuery->value === 0 || $style->mediaQuery->value <= $mediaQueryEnum->value)) {
$items[] = $style;
}
}
return $this->merge($items);
}
private function merge(array $styles): array
{
$tmp = [];
foreach($styles as $style) {
if(isset($tmp[$style->style::class]) && $style->style::class === Padding::class) {
\PHPNative\Tailwind\Parser\Padding::merge($tmp[$style->style::class], $style->style);
}elseif(isset($tmp[$style->style::class]) && $style->style::class === Margin::class) {
\PHPNative\Tailwind\Parser\Margin::merge($tmp[$style->style::class], $style->style);
}else{
$tmp[$style->style::class] = $style->style;
}
}
return $tmp;
}
}

View File

@ -1,21 +1,22 @@
<?php <?php
declare(strict_types=1); declare(strict_types=1);
namespace PHPNative\Tailwind\Parser; namespace PHPNative\Tailwind\Parser;
class Background implements Parser class Background implements Parser
{ {
public static function parse(string $style): \PHPNative\Tailwind\Style\Background public static function parse(string $style): \PHPNative\Tailwind\Style\Background
{ {
$color = new \PHPNative\Tailwind\Style\Color(); $color = new \PHPNative\Tailwind\Style\Color();
preg_match_all('/bg-(.*)/', $style, $output_array); preg_match_all('/bg-(.*)/', $style, $output_array);
if(count($output_array[0]) > 0) { if (count($output_array[0]) > 0) {
$colorStyle = $output_array[1][0]; $colorStyle = $output_array[1][0];
$color = Color::parse($colorStyle); $color = Color::parse($colorStyle);
} }
return new \PHPNative\Tailwind\Style\Background($color); return new \PHPNative\Tailwind\Style\Background($color);
} }
} }

View File

@ -1,11 +1,11 @@
<?php <?php
declare(strict_types=1); declare(strict_types=1);
namespace PHPNative\Tailwind\Parser; namespace PHPNative\Tailwind\Parser;
class Color implements Parser class Color implements Parser
{ {
public static function parse(string $style): \PHPNative\Tailwind\Style\Color public static function parse(string $style): \PHPNative\Tailwind\Style\Color
{ {
$red = 0; $red = 0;
@ -14,17 +14,24 @@ class Color implements Parser
$data = json_decode(file_get_contents(__DIR__ . '/../Data/colors.json'), true); $data = json_decode(file_get_contents(__DIR__ . '/../Data/colors.json'), true);
if($style == "black") {
return new \PHPNative\Tailwind\Style\Color(0, 0, 0);
}
if($style == "white") {
return new \PHPNative\Tailwind\Style\Color(255, 255, 255);
}
preg_match_all('/(\w{1,8})/', $style, $output_array); preg_match_all('/(\w{1,8})/', $style, $output_array);
if(count($output_array[0]) > 0) { if (count($output_array[0]) > 0) {
$color = (string)$output_array[1][0]; $color = (string)$output_array[1][0];
list($red, $green, $blue) = sscanf($data[$color]['500'], "#%02x%02x%02x"); [$red, $green, $blue] = sscanf($data[$color]['500'], "#%02x%02x%02x");
} }
preg_match_all('/(\w{1,8})-(\d{1,3})/', $style, $output_array); preg_match_all('/(\w{1,8})-(\d{1,3})/', $style, $output_array);
if(count($output_array[0]) > 0) { if (count($output_array[0]) > 0) {
$color = (string)$output_array[1][0]; $color = (string)$output_array[1][0];
$variant = (string)$output_array[2][0]; $variant = (string)$output_array[2][0];
list($red, $green, $blue) = sscanf($data[$color][$variant], "#%02x%02x%02x"); [$red, $green, $blue] = sscanf($data[$color][$variant], "#%02x%02x%02x");
} }

View File

@ -0,0 +1,78 @@
<?php
declare(strict_types=1);
namespace PHPNative\Tailwind\Parser;
class Margin implements Parser
{
public static function parse(string $style): ?\PHPNative\Tailwind\Style\Margin
{
$l = null;
$r = null;
$t = null;
$b = null;
preg_match_all('/m-(\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('/mx-(\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('/my-(\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('/mt-(\d*)/', $style, $output_array);
if (count($output_array[0]) > 0) {
$t = (int)$output_array[1][0];
}
preg_match_all('/mb-(\d*)/', $style, $output_array);
if (count($output_array[0]) > 0) {
$b = (int)$output_array[1][0];
}
preg_match_all('/ml-(\d*)/', $style, $output_array);
if (count($output_array[0]) > 0) {
$l = (int)$output_array[1][0];
}
preg_match_all('/mr-(\d*)/', $style, $output_array);
if (count($output_array[0]) > 0) {
$r = (int)$output_array[1][0];
}
if($l != null || $r != null || $t != null || $b != null) {
return new \PHPNative\Tailwind\Style\Margin($l, $r, $t, $b);
}
return null;
}
public static function merge(\PHPNative\Tailwind\Style\Margin $class, \PHPNative\Tailwind\Style\Margin $style)
{
if($style->left != null) {
$class->left = $style->left;
}
if($style->right != null) {
$class->right = $style->right;
}
if($style->top != null) {
$class->top = $style->top;
}
if($style->bottom != null) {
$class->bottom = $style->bottom;
}
}
}

View File

@ -0,0 +1,18 @@
<?php
namespace PHPNative\Tailwind\Parser;
class MediaQuery
{
public static function parse(string $style): ?\PHPNative\Tailwind\Style\MediaQueryEnum
{
preg_match_all("/^(sm|md|lg|xl|2xl)\:(.*)/", $style, $output_array);
if (count($output_array[0]) > 0) {
$query = strtolower(strrev($output_array[1][0]));
return \PHPNative\Tailwind\Style\MediaQueryEnum::{$query};
}
return null;
}
}

View File

@ -1,52 +1,78 @@
<?php <?php
declare(strict_types=1); declare(strict_types=1);
namespace PHPNative\Tailwind\Parser; namespace PHPNative\Tailwind\Parser;
class Padding implements Parser class Padding implements Parser
{ {
public static function parse(string $style): ?\PHPNative\Tailwind\Style\Padding
public static function parse(string $style): \PHPNative\Tailwind\Style\Padding
{ {
$l = 0; $l = null;
$r = 0; $r = null;
$t = 0; $t = null;
$b = 0; $b = null;
preg_match_all('/p-(\d)/', $style, $output_array); preg_match_all('/p-(\d*)/', $style, $output_array);
if(count($output_array[0]) > 0) { if (count($output_array[0]) > 0) {
$l = (int)$output_array[1][0]; $l = (int)$output_array[1][0];
$r = (int)$output_array[1][0]; $r = (int)$output_array[1][0];
$t = (int)$output_array[1][0]; $t = (int)$output_array[1][0];
$b = (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) { preg_match_all('/px-(\d*)/', $style, $output_array);
if (count($output_array[0]) > 0) {
$l = (int)$output_array[1][0]; $l = (int)$output_array[1][0];
$r = (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); 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];
}
if($l != null || $r != null || $t != null || $b != null) {
return new \PHPNative\Tailwind\Style\Padding($l, $r, $t, $b);
}
return null;
}
public static function merge(\PHPNative\Tailwind\Style\Padding $class, \PHPNative\Tailwind\Style\Padding $style)
{
if($style->left != null) {
$class->left = $style->left;
}
if($style->right != null) {
$class->right = $style->right;
}
if($style->top != null) {
$class->top = $style->top;
}
if($style->bottom != null) {
$class->bottom = $style->bottom;
}
} }
} }

View File

@ -1,4 +1,5 @@
<?php <?php
declare(strict_types=1); declare(strict_types=1);
namespace PHPNative\Tailwind\Parser; namespace PHPNative\Tailwind\Parser;
@ -7,5 +8,5 @@ use PHPNative\Tailwind\Style\Style;
interface Parser interface Parser
{ {
public static function parse(string $style): Style; public static function parse(string $style): ?Style;
} }

View File

@ -0,0 +1,18 @@
<?php
namespace PHPNative\Tailwind\Parser;
class State
{
public static function parse(string $style): ?\PHPNative\Tailwind\Style\StateEnum
{
preg_match_all("/(hover|focus|active)\:(.*)/", $style, $output_array);
if (count($output_array[0]) > 0) {
$query = strtolower($output_array[1][0]);
return \PHPNative\Tailwind\Style\StateEnum::{$query};
}
return null;
}
}

View File

@ -0,0 +1,60 @@
<?php
declare(strict_types=1);
namespace PHPNative\Tailwind\Parser;
use PHPNative\Tailwind\Style\Unit;
class Width implements Parser
{
public static function parse(string $style): ?\PHPNative\Tailwind\Style\Width
{
$value = -1;
$unit = Unit::Pixel;
$found = false;
preg_match_all('/w-(\d*)\/(\d*)/', $style, $output_array);
if (count($output_array[0]) > 0) {
$value1 = (int)$output_array[1][0];
$value2 = (int)$output_array[2][0];
$unit = Unit::Percent;
$value = 100/$value2*$value1;
$found = true;
}
preg_match_all('/w-(\d*)/', $style, $output_array);
if (!$found && count($output_array[0]) > 0) {
$value = (int)$output_array[1][0];
}
preg_match_all('/w-full/', $style, $output_array);
if (!$found && count($output_array[0]) > 0) {
$value = 100;
$unit = Unit::Percent;
}
if($value != -1) {
return new \PHPNative\Tailwind\Style\Width($unit, $value);
}
return null;
}
public static function merge(\PHPNative\Tailwind\Style\Padding $class, \PHPNative\Tailwind\Style\Padding $style)
{
if($style->left != null) {
$class->left = $style->left;
}
if($style->right != null) {
$class->right = $style->right;
}
if($style->top != null) {
$class->top = $style->top;
}
if($style->bottom != null) {
$class->bottom = $style->bottom;
}
}
}

View File

@ -1,13 +1,12 @@
<?php <?php
declare(strict_types=1); declare(strict_types=1);
namespace PHPNative\Tailwind\Style; namespace PHPNative\Tailwind\Style;
class Background implements Style class Background implements Style
{ {
public function __construct(public Color $color = new Color()) public function __construct(public Color $color = new Color())
{ {
} }
} }

View File

@ -1,4 +1,5 @@
<?php <?php
declare(strict_types=1); declare(strict_types=1);
namespace PHPNative\Tailwind\Style; namespace PHPNative\Tailwind\Style;

View File

@ -0,0 +1,12 @@
<?php
declare(strict_types=1);
namespace PHPNative\Tailwind\Style;
class Margin implements Style
{
public function __construct(public int|null $left = null, public int|null $right = null, public int|null $top = null, public int|null $bottom = null)
{
}
}

View File

@ -0,0 +1,10 @@
<?php
namespace PHPNative\Tailwind\Style;
class MediaQuery
{
public function __construct(public MediaQueryEnum $mediaQuery, public string $restStyle)
{
}
}

View File

@ -0,0 +1,33 @@
<?php
namespace PHPNative\Tailwind\Style;
enum MediaQueryEnum: int
{
case normal = 0;
case ms = 640;
case dm = 768;
case gl = 1024;
case lx = 1280;
case lx2 = 1536;
public static function getFromPixel(int $windowWidth)
{
if($windowWidth > self::lx2->value) {
return self::lx2;
}
if($windowWidth > self::lx->value) {
return self::lx;
}
if($windowWidth > self::gl->value) {
return self::gl;
}
if($windowWidth > self::dm->value) {
return self::dm;
}
if($windowWidth > self::ms->value) {
return self::ms;
}
return self::normal;
}
}

View File

@ -1,11 +1,12 @@
<?php <?php
declare(strict_types=1); declare(strict_types=1);
namespace PHPNative\Tailwind\Style; namespace PHPNative\Tailwind\Style;
class Padding implements Style class Padding implements Style
{ {
public function __construct(public int $left = 0, public int $right = 0, public int $top = 0, public int $bottom = 0) public function __construct(public int|null $left = null, public int|null $right = null, public int|null $top = null, public int|null $bottom = null)
{ {
} }
} }

View File

@ -0,0 +1,10 @@
<?php
namespace PHPNative\Tailwind\Style;
class State
{
public function __construct(public StateEnum $state, public string $restStyle)
{
}
}

View File

@ -0,0 +1,11 @@
<?php
namespace PHPNative\Tailwind\Style;
enum StateEnum
{
case normal;
case hover;
case focus;
case active;
}

View File

@ -1,4 +1,5 @@
<?php <?php
declare(strict_types=1); declare(strict_types=1);
namespace PHPNative\Tailwind\Style; namespace PHPNative\Tailwind\Style;

View File

@ -0,0 +1,11 @@
<?php
declare(strict_types=1);
namespace PHPNative\Tailwind\Style;
enum Unit
{
case Pixel;
case Point;
case Percent;
}

View File

@ -0,0 +1,12 @@
<?php
declare(strict_types=1);
namespace PHPNative\Tailwind\Style;
class Width implements Style
{
public function __construct(public Unit $unit = Unit::Pixel, public int $value = 0)
{
}
}

View File

@ -1,17 +1,55 @@
<?php <?php
declare(strict_types=1); declare(strict_types=1);
namespace PHPNative\Tailwind; namespace PHPNative\Tailwind;
use PHPNative\Tailwind\Model\Size;
use PHPNative\Tailwind\Model\StyleCollection;
use PHPNative\Tailwind\Style\Margin;
use PHPNative\Tailwind\Style\Padding; use PHPNative\Tailwind\Style\Padding;
use PHPNative\Tailwind\Style\Style; use PHPNative\Tailwind\Style\Style;
class StyleParser class StyleParser
{ {
public static function parse($style, int $width = 0): StyleCollection
public static function parse($style): Style
{ {
return new Padding(); $computed = new StyleCollection();
$styles = explode(" ", $style);
foreach($styles as $styleStr) {
$styleStr = trim($styleStr);
$style = self::parseSimpleStyle($styleStr);
$s = new \PHPNative\Tailwind\Model\Style($style);
$mq = \PHPNative\Tailwind\Parser\MediaQuery::parse($styleStr);
if($mq) {
$s->mediaQuery = $mq;
}
$state = \PHPNative\Tailwind\Parser\State::parse($styleStr);
if($state) {
$s->state = $state;
}
$computed->add($s);
}
return $computed;
} }
private static function parseSimpleStyle(string $style): ?Style
{
if($pd = \PHPNative\Tailwind\Parser\Padding::parse($style)) {
return $pd;
}
if($m = \PHPNative\Tailwind\Parser\Margin::parse($style)) {
return $m;
}
if($w = \PHPNative\Tailwind\Parser\Width::parse($style)) {
return $w;
}
if($bg = \PHPNative\Tailwind\Parser\Background::parse($style)) {
return $bg;
}
return null;
}
} }

View File

@ -1,4 +1,5 @@
<?php <?php
declare(strict_types=1); declare(strict_types=1);
namespace PHPNative\Tailwind\Tests; namespace PHPNative\Tailwind\Tests;
@ -6,10 +7,13 @@ namespace PHPNative\Tailwind\Tests;
use PHPNative\Tailwind\Parser\Background; use PHPNative\Tailwind\Parser\Background;
use PHPUnit\Framework\TestCase; use PHPUnit\Framework\TestCase;
/**
* @internal
* @small
*/
class BackgroundTest extends TestCase class BackgroundTest extends TestCase
{ {
public function test_background(): void
public function testBackground(): void
{ {
$bg = Background::parse("bg-slate-300"); $bg = Background::parse("bg-slate-300");
$this->assertInstanceOf(\PHPNative\Tailwind\Style\Background::class, $bg); $this->assertInstanceOf(\PHPNative\Tailwind\Style\Background::class, $bg);
@ -18,5 +22,4 @@ class BackgroundTest extends TestCase
$this->assertSame(225, $bg->color->blue); $this->assertSame(225, $bg->color->blue);
$this->assertSame(0, $bg->color->alpha); $this->assertSame(0, $bg->color->alpha);
} }
} }

View File

@ -1,4 +1,5 @@
<?php <?php
declare(strict_types=1); declare(strict_types=1);
namespace PHPNative\Tailwind\Tests; namespace PHPNative\Tailwind\Tests;
@ -6,25 +7,49 @@ namespace PHPNative\Tailwind\Tests;
use PHPNative\Tailwind\Parser\Color; use PHPNative\Tailwind\Parser\Color;
use PHPUnit\Framework\TestCase; use PHPUnit\Framework\TestCase;
/**
* @internal
* @small
*/
class ColorTest extends TestCase class ColorTest extends TestCase
{ {
public function testSimpleName(): void public function test_simple_name(): void
{ {
$color = Color::parse("red"); $color = Color::parse("red");
self::assertInstanceOf(\PHPNative\Tailwind\Style\Color::class, $color); $this->assertInstanceOf(\PHPNative\Tailwind\Style\Color::class, $color);
self::assertSame(239, $color->red); $this->assertSame(239, $color->red);
self::assertSame(68, $color->green); $this->assertSame(68, $color->green);
self::assertSame(68, $color->blue); $this->assertSame(68, $color->blue);
self::assertSame(0, $color->alpha); $this->assertSame(0, $color->alpha);
} }
public function testNameVariant(): void public function test_name_variant(): void
{ {
$color = Color::parse("lime-300"); $color = Color::parse("lime-300");
self::assertInstanceOf(\PHPNative\Tailwind\Style\Color::class, $color); $this->assertInstanceOf(\PHPNative\Tailwind\Style\Color::class, $color);
self::assertSame(190, $color->red); $this->assertSame(190, $color->red);
self::assertSame(242, $color->green); $this->assertSame(242, $color->green);
self::assertSame(100, $color->blue); $this->assertSame(100, $color->blue);
self::assertSame(0, $color->alpha); $this->assertSame(0, $color->alpha);
}
public function test_white(): void
{
$color = Color::parse("white");
$this->assertInstanceOf(\PHPNative\Tailwind\Style\Color::class, $color);
$this->assertSame(255, $color->red);
$this->assertSame(255, $color->green);
$this->assertSame(255, $color->blue);
$this->assertSame(0, $color->alpha);
}
public function test_black(): void
{
$color = Color::parse("black");
$this->assertInstanceOf(\PHPNative\Tailwind\Style\Color::class, $color);
$this->assertSame(0, $color->red);
$this->assertSame(0, $color->green);
$this->assertSame(0, $color->blue);
$this->assertSame(0, $color->alpha);
} }
} }

View File

@ -0,0 +1,34 @@
<?php
namespace PHPNative\Tailwind\Tests;
use PHPNative\Renderer\Viewport;
use PHPNative\Tailwind\Model\Size;
use PHPNative\Tailwind\Style\Margin;
use PHPNative\Tailwind\Style\MediaQueryEnum;
use PHPNative\Tailwind\Style\Padding;
use PHPNative\Tailwind\Style\StateEnum;
use PHPNative\Tailwind\StyleParser;
use PHPUnit\Framework\TestCase;
class ComplexTest extends TestCase
{
public function testComplex(): void
{
$computed = StyleParser::parse("bg-lime-300 p-2 pb-3 m-2 ml-10 mr-10")->getValidStyles(MediaQueryEnum::normal, StateEnum::normal);
$this->assertCount(3, $computed);
$this->assertEquals(2, $computed[Padding::class]->left);
$this->assertEquals(3, $computed[Padding::class]->bottom);
$this->assertEquals(2, $computed[Margin::class]->top);
$this->assertEquals(10, $computed[Margin::class]->left);
$this->assertEquals(10, $computed[Margin::class]->right);
}
public function testComplexWithModifier(): void
{
$computed = StyleParser::parse("bg-lime-300 hover:bg-red-200 md:hover:bg-green-100 md:m-50", 200);
$this->assertCount(1, $computed->getValidStyles(MediaQueryEnum::ms, StateEnum::hover));
$this->assertCount(2, $computed->getValidStyles(MediaQueryEnum::lx, StateEnum::active));
$this->assertCount(2, $computed->getValidStyles(MediaQueryEnum::dm, StateEnum::active));
}
}

View File

@ -0,0 +1,95 @@
<?php
declare(strict_types=1);
namespace PHPNative\Tailwind\Tests;
use PHPNative\Tailwind\Parser\Margin;
use PHPUnit\Framework\TestCase;
/**
* @internal
* @small
*/
class MarginTest extends TestCase
{
public function test_margin_overall(): void
{
$margin = Margin::parse("m-6");
$this->assertInstanceOf(\PHPNative\Tailwind\Style\Margin::class, $margin);
$this->assertSame(6, $margin->left);
$this->assertSame(6, $margin->right);
$this->assertSame(6, $margin->top);
$this->assertSame(6, $margin->bottom);
}
public function test_margin_x(): void
{
$margin = Margin::parse("mx-2");
$this->assertInstanceOf(\PHPNative\Tailwind\Style\Margin::class, $margin);
$this->assertSame(2, $margin->left);
$this->assertSame(2, $margin->right);
$this->assertSame(null, $margin->top);
$this->assertSame(null, $margin->bottom);
}
public function test_margin_y(): void
{
$margin = Margin::parse("my-2");
$this->assertInstanceOf(\PHPNative\Tailwind\Style\Margin::class, $margin);
$this->assertSame(null, $margin->left);
$this->assertSame(null, $margin->right);
$this->assertSame(2, $margin->top);
$this->assertSame(2, $margin->bottom);
}
public function test_margin_t(): void
{
$margin = Margin::parse("mt-3");
$this->assertInstanceOf(\PHPNative\Tailwind\Style\Margin::class, $margin);
$this->assertSame(null, $margin->left);
$this->assertSame(null, $margin->right);
$this->assertSame(3, $margin->top);
$this->assertSame(null, $margin->bottom);
}
public function test_margin_b(): void
{
$margin = Margin::parse("mb-3");
$this->assertInstanceOf(\PHPNative\Tailwind\Style\Margin::class, $margin);
$this->assertSame(null, $margin->left);
$this->assertSame(null, $margin->right);
$this->assertSame(null, $margin->top);
$this->assertSame(3, $margin->bottom);
}
public function test_margin_l(): void
{
$margin = Margin::parse("ml-3");
$this->assertInstanceOf(\PHPNative\Tailwind\Style\Margin::class, $margin);
$this->assertSame(3, $margin->left);
$this->assertSame(null, $margin->right);
$this->assertSame(null, $margin->top);
$this->assertSame(null, $margin->bottom);
}
public function test_margin_r(): void
{
$margin = Margin::parse("mr-3");
$this->assertInstanceOf(\PHPNative\Tailwind\Style\Margin::class, $margin);
$this->assertSame(null, $margin->left);
$this->assertSame(3, $margin->right);
$this->assertSame(null, $margin->top);
$this->assertSame(null, $margin->bottom);
}
public function test_margin_complex(): void
{
$margin = Margin::parse("m-2 ml-1 mr-3");
$this->assertInstanceOf(\PHPNative\Tailwind\Style\Margin::class, $margin);
$this->assertSame(1, $margin->left);
$this->assertSame(3, $margin->right);
$this->assertSame(2, $margin->top);
$this->assertSame(2, $margin->bottom);
}
}

View File

@ -1,5 +1,7 @@
<?php <?php
declare(strict_types=1);
namespace PHPNative\Tailwind\Tests; namespace PHPNative\Tailwind\Tests;
use PHPNative\Tailwind\Parser\Padding; use PHPNative\Tailwind\Parser\Padding;
@ -7,83 +9,83 @@ use PHPUnit\Framework\TestCase;
class PaddingTest extends TestCase class PaddingTest extends TestCase
{ {
public function testPaddingOverall(): void public function test_padding_overall(): void
{ {
$padding = Padding::parse("p-6"); $padding = Padding::parse("p-6");
self::assertInstanceOf(\PHPNative\Tailwind\Style\Padding::class, $padding); $this->assertInstanceOf(\PHPNative\Tailwind\Style\Padding::class, $padding);
self::assertSame(6, $padding->left); $this->assertSame(6, $padding->left);
self::assertSame(6, $padding->right); $this->assertSame(6, $padding->right);
self::assertSame(6, $padding->top); $this->assertSame(6, $padding->top);
self::assertSame(6, $padding->bottom); $this->assertSame(6, $padding->bottom);
} }
public function testPaddingX(): void public function test_padding_x(): void
{ {
$padding = Padding::parse("px-2"); $padding = Padding::parse("px-2");
self::assertInstanceOf(\PHPNative\Tailwind\Style\Padding::class, $padding); $this->assertInstanceOf(\PHPNative\Tailwind\Style\Padding::class, $padding);
self::assertSame(2, $padding->left); $this->assertSame(2, $padding->left);
self::assertSame(2, $padding->right); $this->assertSame(2, $padding->right);
self::assertSame(0, $padding->top); $this->assertSame(null, $padding->top);
self::assertSame(0, $padding->bottom); $this->assertSame(null, $padding->bottom);
} }
public function testPaddingY(): void public function test_padding_y(): void
{ {
$padding = Padding::parse("py-2"); $padding = Padding::parse("py-2");
self::assertInstanceOf(\PHPNative\Tailwind\Style\Padding::class, $padding); $this->assertInstanceOf(\PHPNative\Tailwind\Style\Padding::class, $padding);
self::assertSame(0, $padding->left); $this->assertSame(null, $padding->left);
self::assertSame(0, $padding->right); $this->assertSame(null, $padding->right);
self::assertSame(2, $padding->top); $this->assertSame(2, $padding->top);
self::assertSame(2, $padding->bottom); $this->assertSame(2, $padding->bottom);
} }
public function testPaddingT(): void public function test_padding_t(): void
{ {
$padding = Padding::parse("pt-3"); $padding = Padding::parse("pt-3");
self::assertInstanceOf(\PHPNative\Tailwind\Style\Padding::class, $padding); $this->assertInstanceOf(\PHPNative\Tailwind\Style\Padding::class, $padding);
self::assertSame(0, $padding->left); $this->assertSame(null, $padding->left);
self::assertSame(0, $padding->right); $this->assertSame(null, $padding->right);
self::assertSame(3, $padding->top); $this->assertSame(3, $padding->top);
self::assertSame(0, $padding->bottom); $this->assertSame(null, $padding->bottom);
} }
public function testPaddingB(): void public function test_padding_b(): void
{ {
$padding = Padding::parse("pb-3"); $padding = Padding::parse("pb-3");
self::assertInstanceOf(\PHPNative\Tailwind\Style\Padding::class, $padding); $this->assertInstanceOf(\PHPNative\Tailwind\Style\Padding::class, $padding);
self::assertSame(0, $padding->left); $this->assertSame(null, $padding->left);
self::assertSame(0, $padding->right); $this->assertSame(null, $padding->right);
self::assertSame(0, $padding->top); $this->assertSame(null, $padding->top);
self::assertSame(3, $padding->bottom); $this->assertSame(3, $padding->bottom);
} }
public function testPaddingL(): void public function test_padding_l(): void
{ {
$padding = Padding::parse("pl-3"); $padding = Padding::parse("pl-3");
self::assertInstanceOf(\PHPNative\Tailwind\Style\Padding::class, $padding); $this->assertInstanceOf(\PHPNative\Tailwind\Style\Padding::class, $padding);
self::assertSame(3, $padding->left); $this->assertSame(3, $padding->left);
self::assertSame(0, $padding->right); $this->assertSame(null, $padding->right);
self::assertSame(0, $padding->top); $this->assertSame(null, $padding->top);
self::assertSame(0, $padding->bottom); $this->assertSame(null, $padding->bottom);
} }
public function testPaddingR(): void public function test_padding_r(): void
{ {
$padding = Padding::parse("pr-3"); $padding = Padding::parse("pr-3");
self::assertInstanceOf(\PHPNative\Tailwind\Style\Padding::class, $padding); $this->assertInstanceOf(\PHPNative\Tailwind\Style\Padding::class, $padding);
self::assertSame(0, $padding->left); $this->assertSame(null, $padding->left);
self::assertSame(3, $padding->right); $this->assertSame(3, $padding->right);
self::assertSame(0, $padding->top); $this->assertSame(null, $padding->top);
self::assertSame(0, $padding->bottom); $this->assertSame(null, $padding->bottom);
} }
public function testPaddingComplex(): void public function test_padding_complex(): void
{ {
$padding = Padding::parse("p-2 pl-1 pr-3"); $padding = Padding::parse("p-2 pl-1 pr-3");
self::assertInstanceOf(\PHPNative\Tailwind\Style\Padding::class, $padding); $this->assertInstanceOf(\PHPNative\Tailwind\Style\Padding::class, $padding);
self::assertSame(1, $padding->left); $this->assertSame(1, $padding->left);
self::assertSame(3, $padding->right); $this->assertSame(3, $padding->right);
self::assertSame(2, $padding->top); $this->assertSame(2, $padding->top);
self::assertSame(2, $padding->bottom); $this->assertSame(2, $padding->bottom);
} }
} }

View File

@ -0,0 +1,44 @@
<?php
declare(strict_types=1);
namespace PHPNative\Tailwind\Tests;
use PHPNative\Tailwind\Parser\Width;
use PHPNative\Tailwind\Style\Unit;
use PHPUnit\Framework\TestCase;
class WidthTest extends TestCase
{
public function test_width_pixel(): void
{
$width = Width::parse("w-10");
$this->assertInstanceOf(\PHPNative\Tailwind\Style\Width::class, $width);
$this->assertSame(10, $width->value);
$this->assertSame(Unit::Pixel, $width->unit);
}
public function test_width_percent_full(): void
{
$width = Width::parse("w-full");
$this->assertInstanceOf(\PHPNative\Tailwind\Style\Width::class, $width);
$this->assertSame(100, $width->value);
$this->assertSame(Unit::Percent, $width->unit);
}
public function test_width_percent_one_half(): void
{
$width = Width::parse("w-1/2");
$this->assertInstanceOf(\PHPNative\Tailwind\Style\Width::class, $width);
$this->assertSame(50, $width->value);
$this->assertSame(Unit::Percent, $width->unit);
}
public function test_width_percent_second_third(): void
{
$width = Width::parse("w-3/4");
$this->assertInstanceOf(\PHPNative\Tailwind\Style\Width::class, $width);
$this->assertSame(75, $width->value);
$this->assertSame(Unit::Percent, $width->unit);
}
}

View File

@ -1,8 +0,0 @@
<?php
namespace PHPNative\UI;
interface View
{
public function getWidget(): Widget;
}

View File

@ -1,8 +0,0 @@
<?php
namespace PHPNative\UI;
interface Widget
{
}

Some files were not shown because too many files have changed in this diff Show More