diff --git a/.php-cs-fixer.dist.php b/.php-cs-fixer.dist.php new file mode 100644 index 0000000..78b930d --- /dev/null +++ b/.php-cs-fixer.dist.php @@ -0,0 +1,88 @@ +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); diff --git a/assets/segoe-ui.ttf b/assets/segoe-ui.ttf new file mode 100644 index 0000000..30da655 Binary files /dev/null and b/assets/segoe-ui.ttf differ diff --git a/composer.json b/composer.json index 483ad59..d8d748f 100644 --- a/composer.json +++ b/composer.json @@ -1,28 +1,7 @@ { "name": "phpnative/framework", - "type": "library", "license": "MIT", - "autoload": { - "psr-4": { - "PHPNative\\Framework\\": "src/PHPNative/Framework/src", - "PHPNative\\UI\\": "src/PHPNative/UI/src", - "PHPNative\\Tailwind\\": "src/PHPNative/Tailwind/src", - "PHPNative\\Core\\": "src/PHPNative/Core/src", - "PHPNative\\Container\\": "src/PHPNative/Container/src" - } - }, - "replace": { - "phpnative/tailwind": "self.version" - }, - "autoload-dev": { - "psr-4": { - "PHPNative\\Tailwind\\Tests\\": "src/PHPNative/Tailwind/tests", - "PHPNative\\Core\\Tests\\": "src/PHPNative/Core/tests", - "PHPNative\\UI\\Tests\\": "src/PHPNative/UI/tests", - "PHPNative\\Container\\Tests\\": "src/PHPNative/Container/tests", - "PHPNative\\Framework\\Tests\\": "src/PHPNative/Framework/tests" - } - }, + "type": "library", "authors": [ { "name": "Thomas Peterson", @@ -30,8 +9,9 @@ } ], "require": { + "ext-parallel": "*", "ext-sdl": "*", - "ext-parallel": "*" + "php": "^8.3" }, "require-dev": { "friendsofphp/php-cs-fixer": "^3.21", @@ -42,6 +22,40 @@ "spaze/phpstan-disallowed-calls": "^3.1", "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": { "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", @@ -51,7 +65,6 @@ "merge": "vendor/bin/monorepo-builder merge", "qa": [ "composer merge", - "./tempest discovery:clear", "vendor/bin/rector process", "composer csfixer", "composer phpunit", diff --git a/rector.php b/rector.php new file mode 100644 index 0000000..29d7e80 --- /dev/null +++ b/rector.php @@ -0,0 +1,75 @@ +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); diff --git a/src/PHPNative/Container/src/Container.php b/src/PHPNative/Container/src/Container.php index 26b11da..ea9c7de 100644 --- a/src/PHPNative/Container/src/Container.php +++ b/src/PHPNative/Container/src/Container.php @@ -1,33 +1,34 @@ - $className - * @return TClassName - */ - public function get(string $className, mixed ...$params): object; - - public function call(object $object, string $methodName, mixed ...$params): mixed; - - /** - * @template T of \PHPNative\Container\src\Initializer - * @template U of \PHPNative\Container\src\DynamicInitializer - * @param ReflectionClass|class-string|class-string $initializerClass - * @return self - */ - public function addInitializer(ReflectionClass|string $initializerClass): self; -} \ No newline at end of file + $className + * @return TClassName + */ + public function get(string $className, ?string $tag = null, mixed ...$params): object; + + public function invoke(MethodReflector $method, mixed ...$params): mixed; + + /** + * @template T of \PHPNative\Container\Initializer + * @template U of \PHPNative\Container\DynamicInitializer + * @param ClassReflector|class-string|class-string $initializerClass + */ + public function addInitializer(ClassReflector|string $initializerClass): self; +} diff --git a/src/PHPNative/Container/src/ContainerLog.php b/src/PHPNative/Container/src/ContainerLog.php deleted file mode 100644 index 01d48e3..0000000 --- a/src/PHPNative/Container/src/ContainerLog.php +++ /dev/null @@ -1,25 +0,0 @@ -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, - ) - ); - } -} diff --git a/src/PHPNative/Container/src/Dependency.php b/src/PHPNative/Container/src/Dependency.php index 9f770b4..9b2b33a 100644 --- a/src/PHPNative/Container/src/Dependency.php +++ b/src/PHPNative/Container/src/Dependency.php @@ -1,87 +1,88 @@ -typeToString($this->getType()); - } - - public function __toString(): string - { - $typeToString = $this->typeToString($this->getType()); - $parts = explode('\\', $typeToString); - $typeToString = $parts[array_key_last($parts)]; - - return implode( - ' ', - array_filter([ - $typeToString, - '$' . $this->reflector->getName(), - ]), - ); - } - - private function getType(): string|ReflectionType - { - return match($this->reflector::class) { - ReflectionParameter::class => $this->reflector->getType(), - ReflectionClass::class => $this->reflector->getName(), - }; - } - - private function typeToString(string|ReflectionType|null $type): ?string - { - if ($type === null) { - return null; - } - - if (is_string($type)) { - return $type; - } - - return match($type::class) { - ReflectionIntersectionType::class => $this->intersectionTypeToString($type), - ReflectionNamedType::class => $type->getName(), - ReflectionUnionType::class => $this->unionTypeToString($type), - }; - } - - private function intersectionTypeToString(ReflectionIntersectionType $type): string - { - return implode( - '&', - array_map( - fn (ReflectionType $subType) => $this->typeToString($subType), - $type->getTypes(), - ), - ); - } - - private function unionTypeToString(ReflectionUnionType $type): string - { - return implode( - '|', - array_map( - fn (ReflectionType $subType) => $this->typeToString($subType), - $type->getTypes(), - ), - ); - } -} +resolveName($this->dependency); + } + + public function getShortName(): string + { + return $this->resolveShortName($this->dependency); + } + + public function equals(self $other): bool + { + return $this->getName() === $other->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 resolveName(Reflector|Closure|string $dependency): string + { + if (is_string($dependency)) { + return $dependency; + } + + return match($dependency::class) { + FunctionReflector::class => $dependency->getName() . ' in ' . $dependency->getFileName() . ':' . $dependency->getStartLine(), + ClassReflector::class => $dependency->getName(), + MethodReflector::class => $dependency->getDeclaringClass()->getName() . '::' . $dependency->getName(), + ParameterReflector::class => $dependency->getType()->getName(), + TypeReflector::class => $dependency->getName(), + default => 'unknown', + }; + } + + private function resolveShortName(Reflector|Closure|string $dependency): string + { + if (is_string($dependency)) { + return $dependency; + } + + return match($dependency::class) { + FunctionReflector::class => $dependency->getShortName() . ' in ' . $dependency->getFileName() . ':' . $dependency->getStartLine(), + ClassReflector::class => $dependency->getShortName(), + MethodReflector::class => $dependency->getShortName(), + ParameterReflector::class => $dependency->getType()->getShortName(), + TypeReflector::class => $dependency->getShortName(), + default => 'unknown', + }; + } +} diff --git a/src/PHPNative/Container/src/DependencyChain.php b/src/PHPNative/Container/src/DependencyChain.php new file mode 100644 index 0000000..079932b --- /dev/null +++ b/src/PHPNative/Container/src/DependencyChain.php @@ -0,0 +1,58 @@ +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; + } +} \ No newline at end of file diff --git a/src/PHPNative/Container/src/DynamicInitializer.php b/src/PHPNative/Container/src/DynamicInitializer.php index fcc7b28..32deed8 100644 --- a/src/PHPNative/Container/src/DynamicInitializer.php +++ b/src/PHPNative/Container/src/DynamicInitializer.php @@ -1,12 +1,12 @@ -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: '/(?(.*))(?'. $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); + } +} \ No newline at end of file diff --git a/src/PHPNative/Container/src/Exceptions/CannotInstantiateDependencyException.php b/src/PHPNative/Container/src/Exceptions/CannotInstantiateDependencyException.php index 79d6acd..e3067c5 100644 --- a/src/PHPNative/Container/src/Exceptions/CannotInstantiateDependencyException.php +++ b/src/PHPNative/Container/src/Exceptions/CannotInstantiateDependencyException.php @@ -2,19 +2,20 @@ declare(strict_types=1); -namespace PHPNative\Container\src\Exceptions; +namespace PHPNative\Container\Exceptions; use Exception; -use PHPNative\Container\src\ContainerLog; +use PHPNative\Container\DependencyChain; +use PHPNative\Support\Reflection\ClassReflector; use ReflectionClass; 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; - $stack = $containerLog->getStack(); + $stack = $chain->all(); if ($stack === []) { parent::__construct($message); @@ -22,11 +23,9 @@ final class CannotInstantiateDependencyException extends Exception return; } - $lastContext = $stack[array_key_last($stack)]; - $i = 0; - foreach ($stack as $currentContext) { + foreach ($stack as $currentDependency) { $pipe = match (true) { count($stack) > 1 && $i === 0 => '┌──', count($stack) > 1 && $i === count($stack) - 1 => '└──', @@ -34,23 +33,24 @@ final class CannotInstantiateDependencyException extends Exception default => '├──', }; - $message .= PHP_EOL . "\t{$pipe} " . $currentContext; + $message .= PHP_EOL . "\t{$pipe} " . $currentDependency->getShortName(); $i++; } - $currentDependency = $lastContext->currentDependency(); - $currentDependencyName = (string)$currentDependency; - $firstPart = explode($currentDependencyName, (string)$lastContext)[0] ?? null; - $fillerSpaces = str_repeat(' ', strlen($firstPart) + 3); - $fillerArrows = str_repeat('▒', strlen($currentDependencyName)); - $message .= PHP_EOL . "\t {$fillerSpaces}{$fillerArrows}"; + $lastDependency = $chain->last(); + // $currentDependencyName = $lastDependency->getShortName(); + // $firstPart = explode($currentDependencyName, (string)$lastDependency)[0] ?? null; + // $fillerSpaces = str_repeat(' ', strlen($firstPart) + 3); + // $fillerArrows = str_repeat('▒', strlen($currentDependencyName)); + // $message .= PHP_EOL . "\t {$fillerSpaces}{$fillerArrows}"; + // + // $message .= PHP_EOL . PHP_EOL; - $message .= PHP_EOL . PHP_EOL; - - $message .= "Originally called in {$containerLog->getOrigin()}"; + $message .= "Originally called in {$chain->getOrigin()}"; $message .= PHP_EOL; parent::__construct($message); } } + diff --git a/src/PHPNative/Container/src/GenericContainer.php b/src/PHPNative/Container/src/GenericContainer.php index efe558c..0f0e3e1 100644 --- a/src/PHPNative/Container/src/GenericContainer.php +++ b/src/PHPNative/Container/src/GenericContainer.php @@ -1,336 +1,397 @@ - $initializers - */ - private array $initializers = [], - - /** - * @template T of \PHPNative\Container\src\DynamicInitializer - * @var class-string $dynamicInitializers - */ - private array $dynamicInitializers = [], - private readonly ContainerLog $log = new InMemoryContainerLog(), - ) { - } - - public function setInitializers(array $initializers): void - { - $this->initializers = $initializers; - } - - public function getInitializers(): array - { - return $this->initializers; - } - - public function register(string $className, callable $definition): self - { - $this->definitions[$className] = $definition; - - return $this; - } - - public function singleton(string $className, callable $definition): self - { - $this->definitions[$className] = function () use ($definition, $className) { - $instance = $definition($this); - - $this->singletons[$className] = $instance; - - return $instance; - }; - - return $this; - } - - public function config(object $config): self - { - $this->singleton($config::class, fn () => $config); - - return $this; - } - - public function get(string $className, mixed ...$params): object - { - $this->log->startResolving(); - - return $this->resolve($className, ...$params); - } - - public function call(string|object $object, string $methodName, ...$params): mixed - { - $this->log->startResolving(); - - $object = is_string($object) ? $this->get($object) : $object; - - $reflectionMethod = (new ReflectionClass($object))->getMethod($methodName); - - $parameters = $this->autowireDependencies($reflectionMethod, $params); - - return $reflectionMethod->invokeArgs($object, $parameters); - } - - public function addInitializer(ReflectionClass|string $initializerClass): Container - { - $initializerClass = $initializerClass instanceof ReflectionClass - ? $initializerClass - : new ReflectionClass($initializerClass); - - // First, we check whether this is a DynamicInitializer, - // which don't have a one-to-one mapping - if ($initializerClass->implementsInterface(DynamicInitializer::class)) { - $this->dynamicInitializers[] = $initializerClass->getName(); - - return $this; - } - - // For normal Initializers, we'll use the return type - // to determine which dependency they resolve - $returnTypes = $initializerClass->getMethod('initialize')->getReturnType(); - - $returnTypes = match ($returnTypes::class) { - ReflectionNamedType::class => [$returnTypes], - ReflectionUnionType::class, ReflectionIntersectionType::class => $returnTypes->getTypes(), - }; - - /** @var ReflectionNamedType[] $returnTypes */ - foreach ($returnTypes as $returnType) { - $this->initializers[$returnType->getName()] = $initializerClass->getName(); - } - - return $this; - } - - private function resolve(string $className, mixed ...$params): object - { - // Check if the class has been registered as a singleton. - if ($instance = $this->singletons[$className] ?? null) { - $this->log->addContext(new Context(new ReflectionClass($className))); - - return $instance; - } - - // Check if a callable has been registered to resolve this class. - if ($definition = $this->definitions[$className] ?? null) { - $this->log->addContext(new Context(new ReflectionFunction($definition))); - - return $definition($this); - } - - // Next we check if any of our default initializers can initialize this class. - // If there's an initializer, we don't keep track of the log anymore, - // since initializers are outside the container's responsibility. - if ($initializer = $this->initializerFor($className)) { - $object = match (true) { - $initializer instanceof Initializer => $initializer->initialize($this), - $initializer instanceof DynamicInitializer => $initializer->initialize($className, $this), - }; - // Check whether the initializer's result should be registered as a singleton - if (attribute(Singleton::class)->in($initializer::class)->first() !== null) { - $this->singleton($className, fn () => $object); - - return $this->get($className); - } - - return $object; - } - - // Finally, autowire the class. - return $this->autowire($className, ...$params); - } - - private function initializerFor(string $className): null|Initializer|DynamicInitializer - { - // Initializers themselves can't be initialized, - // otherwise you'd end up with infinite loops - if ( - is_a($className, Initializer::class, true) - || is_a($className, DynamicInitializer::class, true) - ) { - return null; - } - - if ($initializerClass = $this->initializers[$className] ?? null) { - return $this->resolve($initializerClass); - } - - // Loop through the registered initializers to see if - // we have something to handle this class. - foreach ($this->dynamicInitializers as $initializerClass) { - $initializer = $this->resolve($initializerClass); - - if (! $initializer->canInitialize($className)) { - continue; - } - - return $initializer; - } - - return null; - } - - private function autowire(string $className, mixed ...$params): object - { - $reflectionClass = new ReflectionClass($className); - - $constructor = $reflectionClass->getConstructor(); - - if (! $reflectionClass->isInstantiable()) { - throw new CannotInstantiateDependencyException($reflectionClass, $this->log); - } - - return $constructor === null - // If there isn't a constructor, don't waste time - // trying to build it. - ? $reflectionClass->newInstanceWithoutConstructor() - - // Otherwise, use our autowireDependencies helper to automagically - // build up each parameter. - : $reflectionClass->newInstanceArgs( - $this->autowireDependencies($constructor, $params), - ); - } - - /** - * @return ReflectionParameter[] - */ - private function autowireDependencies(ReflectionMethod $method, array $parameters = []): array - { - $this->log->addContext(new Context($method)); - - $dependencies = []; - - // Build the class by iterating through its - // dependencies and resolving them. - foreach ($method->getParameters() as $parameter) { - $dependencies[] = $this->autowireDependency( - parameter: $parameter, - providedValue: $parameters[$parameter->getName()] ?? null, - ); - } - - return $dependencies; - } - - private function autowireDependency(ReflectionParameter $parameter, mixed $providedValue = null): mixed - { - $this->log->addDependency(new Dependency($parameter)); - - $parameterType = $parameter->getType(); - - // If the parameter is a built-in type, immediately skip reflection - // stuff and attempt to give it a default or null value. - if ($parameterType instanceof ReflectionNamedType && $parameterType->isBuiltin()) { - return $this->autowireBuiltinDependency($parameter, $providedValue); - } - - // Convert the types to an array regardless, so we can handle - // union types and single types the same. - $types = match ($parameterType::class) { - ReflectionNamedType::class => [$parameterType], - ReflectionUnionType::class, ReflectionIntersectionType::class => $parameterType->getTypes(), - }; - - // Loop through each type until we hit a match. - foreach ($types as $type) { - try { - return $this->autowireObjectDependency($type, $providedValue); - } catch (Throwable $throwable) { - // We were unable to resolve the dependency for the last union - // type, so we are moving on to the next one. We hang onto - // the exception in case it is a circular reference. - $lastThrowable = $throwable; - } - } - - // If the dependency has a default value, we do our best to prevent - // an error by using that. - if ($parameter->isDefaultValueAvailable()) { - return $parameter->getDefaultValue(); - } - - // At this point, there is nothing else we can do; we don't know - // how to autowire this dependency. - throw $lastThrowable ?? new CannotAutowireException($this->log); - } - - private function autowireObjectDependency(ReflectionNamedType $type, mixed $providedValue): mixed - { - // If the provided value is of the right type, - // don't waste time autowiring, return it! - if (is_a($providedValue, $type->getName())) { - return $providedValue; - } - - // If we can successfully retrieve an instance - // of the necessary dependency, return it. - if ($instance = $this->resolve($type->getName())) { - return $instance; - } - - // At this point, there is nothing else we can do; we don't know - // how to autowire this dependency. - throw new CannotAutowireException($this->log); - } - - private function autowireBuiltinDependency(ReflectionParameter $parameter, mixed $providedValue): mixed - { - // Due to type coercion, the provided value may (or may not) work. - // Here we give up trying to do type work for people. If they - // didn't provide the right type, that's on them. - if ($providedValue !== null) { - return $providedValue; - } - - // If the dependency has a default value, we might as well - // use that at this point. - if ($parameter->isDefaultValueAvailable()) { - return $parameter->getDefaultValue(); - } - - // If the dependency's type is an array or variadic variable, we'll - // try to prevent an error by returning an empty array. - if ( - $parameter->getType()?->getName() === 'array' || - $parameter->isVariadic() - ) { - return []; - } - - // If the dependency's type allows null or is optional, we'll - // try to prevent an error by returning null. - if ($parameter->allowsNull() || $parameter->isOptional()) { - return null; - } - - // At this point, there is nothing else we can do; we don't know - // how to autowire this dependency. - throw new CannotAutowireException($this->log); - } -} \ No newline at end of file + $definitions */ + private ArrayIterator $definitions = new ArrayIterator(), + + /** @var ArrayIterator $singletons */ + private ArrayIterator $singletons = new ArrayIterator(), + + /** @var ArrayIterator $initializers */ + private ArrayIterator $initializers = new ArrayIterator(), + + /** @var ArrayIterator $dynamicInitializers */ + private ArrayIterator $dynamicInitializers = new ArrayIterator(), + private ?DependencyChain $chain = null, + ) { + } + + public function setInitializers(array $initializers): self + { + $this->initializers = new ArrayIterator($initializers); + + return $this; + } + + public function setDynamicInitializers(array $dynamicInitializers): self + { + $this->dynamicInitializers = new ArrayIterator($dynamicInitializers); + + return $this; + } + + public function getInitializers(): array + { + return $this->initializers->getArrayCopy(); + } + + public function getDynamicInitializers(): array + { + return $this->dynamicInitializers->getArrayCopy(); + } + + public function register(string $className, callable $definition): self + { + $this->definitions[$className] = $definition; + + return $this; + } + + public function singleton(string $className, object|callable $definition, ?string $tag = null): self + { + $className = $this->resolveTaggedName($className, $tag); + + $this->singletons[$className] = $definition; + + return $this; + } + + public function config(object $config): self + { + $this->singleton($config::class, $config); + + return $this; + } + + public function get(string $className, ?string $tag = null, mixed ...$params): object + { + $this->resolveChain(); + + $dependency = $this->resolve( + className: $className, + tag: $tag, + params: $params, + ); + + $this->stopChain(); + + return $dependency; + } + + public function invoke(MethodReflector $method, mixed ...$params): mixed + { + $this->resolveChain(); + + $object = $this->get($method->getDeclaringClass()->getName()); + + $parameters = $this->autowireDependencies($method, $params); + + $this->stopChain(); + + return $method->invokeArgs($object, $parameters); + } + + public function addInitializer(ClassReflector|string $initializerClass): Container + { + if (! $initializerClass instanceof ClassReflector) { + $initializerClass = new ClassReflector($initializerClass); + } + + // First, we check whether this is a DynamicInitializer, + // which don't have a one-to-one mapping + if ($initializerClass->getType()->matches(DynamicInitializer::class)) { + $this->dynamicInitializers[] = $initializerClass->getName(); + + 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 + // to determine which dependency they resolve + $returnType = $initializeMethod->getReturnType(); + + foreach ($returnType->split() as $type) { + $this->initializers[$this->resolveTaggedName($type->getName(), $singleton?->tag)] = $initializerClass->getName(); + } + + return $this; + } + + 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. + if ($instance = $this->singletons[$dependencyName] ?? null) { + if ($instance instanceof Closure) { + $instance = $instance($this); + $this->singletons[$className] = $instance; + } + + $this->resolveChain()->add($class); + + return $instance; + } + + // Check if a callable has been registered to resolve this class. + if ($definition = $this->definitions[$dependencyName] ?? null) { + $this->resolveChain()->add(new FunctionReflector($definition)); + + return $definition($this); + } + + // Next we check if any of our default initializers can initialize this class. + if (($initializer = $this->initializerFor($class, $tag)) !== null) { + $initializerClass = new ClassReflector($initializer); + + $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; + } + + // 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. + return $this->autowire($className, ...$params); + } + + private function initializerFor(ClassReflector $class, ?string $tag = null): null|Initializer|DynamicInitializer + { + // Initializers themselves can't be initialized, + // otherwise you'd end up with infinite loops + if ($class->getType()->matches(Initializer::class) || $class->getType()->matches(DynamicInitializer::class)) { + return null; + } + + if ($initializerClass = $this->initializers[$this->resolveTaggedName($class, $tag)] ?? null) { + return $this->resolve($initializerClass); + } + + // Loop through the registered initializers to see if + // we have something to handle this class. + foreach ($this->dynamicInitializers as $initializerClass) { + /** @var DynamicInitializer $initializer */ + $initializer = $this->resolve($initializerClass); + + if (! $initializer->canInitialize($class)) { + continue; + } + + return $initializer; + } + + return null; + } + + private function autowire(string $className, mixed ...$params): object + { + $classReflector = new ClassReflector($className); + + $constructor = $classReflector->getConstructor(); + + if (! $classReflector->isInstantiable()) { + throw new CannotInstantiateDependencyException($classReflector, $this->chain); + } + + $instance = $constructor === null + // If there isn't a constructor, don't waste time + // trying to build it. + ? $classReflector->newInstanceWithoutConstructor() + + // Otherwise, use our autowireDependencies helper to automagically + // build up each parameter. + : $classReflector->newInstanceArgs( + $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 ParameterReflector[] + */ + private function autowireDependencies(MethodReflector $method, array $parameters = []): array + { + $this->resolveChain()->add($method); + + $dependencies = []; + + // Build the class by iterating through its + // dependencies and resolving them. + foreach ($method->getParameters() as $parameter) { + $dependencies[] = $this->clone()->autowireDependency( + parameter: $parameter, + tag: $parameter->getAttribute(Tag::class)?->name, + providedValue: $parameters[$parameter->getName()] ?? null, + ); + } + + return $dependencies; + } + + private function autowireDependency(ParameterReflector $parameter, ?string $tag, mixed $providedValue = null): mixed + { + $parameterType = $parameter->getType(); + + // If the parameter is a built-in type, immediately skip reflection + // stuff and attempt to give it a default or null value. + if ($parameterType->isBuiltin()) { + return $this->autowireBuiltinDependency($parameter, $providedValue); + } + + // Loop through each type until we hit a match. + foreach ($parameter->getType()->split() as $type) { + try { + return $this->autowireObjectDependency( + type: $type, + tag: $tag, + providedValue: $providedValue + ); + } catch (Throwable $throwable) { + // We were unable to resolve the dependency for the last union + // type, so we are moving on to the next one. We hang onto + // the exception in case it is a circular reference. + $lastThrowable = $throwable; + } + } + + // If the dependency has a default value, we do our best to prevent + // an error by using that. + if ($parameter->hasDefaultValue()) { + return $parameter->getDefaultValue(); + } + + // At this point, there is nothing else we can do; we don't know + // how to autowire this dependency. + throw $lastThrowable ?? new CannotAutowireException($this->chain, new Dependency($parameter)); + } + + private function autowireObjectDependency(TypeReflector $type, ?string $tag, mixed $providedValue): mixed + { + // If the provided value is of the right type, + // don't waste time autowiring, return it! + if ($type->accepts($providedValue)) { + return $providedValue; + } + + // If we can successfully retrieve an instance + // of the necessary dependency, return it. + return $this->resolve(className: $type->getName(), tag: $tag); + } + + private function autowireBuiltinDependency(ParameterReflector $parameter, mixed $providedValue): mixed + { + // Due to type coercion, the provided value may (or may not) work. + // Here we give up trying to do type work for people. If they + // didn't provide the right type, that's on them. + if ($providedValue !== null) { + return $providedValue; + } + + // If the dependency has a default value, we might as well + // use that at this point. + if ($parameter->hasDefaultValue()) { + return $parameter->getDefaultValue(); + } + + // If the dependency's type is an array or variadic variable, we'll + // try to prevent an error by returning an empty array. + if ($parameter->isVariadic() || $parameter->isIterable()) { + return []; + } + + // If the dependency's type allows null or is optional, we'll + // try to prevent an error by returning null. + if (! $parameter->isRequired()) { + return null; + } + + // At this point, there is nothing else we can do; we don't know + // how to autowire this dependency. + throw new CannotAutowireException($this->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; + } +} diff --git a/src/PHPNative/Container/src/HasInstance.php b/src/PHPNative/Container/src/HasInstance.php index 7fdb6c5..79b0599 100644 --- a/src/PHPNative/Container/src/HasInstance.php +++ b/src/PHPNative/Container/src/HasInstance.php @@ -1,20 +1,20 @@ -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; - } -} diff --git a/src/PHPNative/Container/src/Initializer.php b/src/PHPNative/Container/src/Initializer.php index bc3f1d3..5bacc1c 100644 --- a/src/PHPNative/Container/src/Initializer.php +++ b/src/PHPNative/Container/src/Initializer.php @@ -1,10 +1,10 @@ -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(); + } +} \ No newline at end of file diff --git a/src/PHPNative/Core/src/PathHelper.php b/src/PHPNative/Core/src/PathHelper.php new file mode 100644 index 0000000..b71d332 --- /dev/null +++ b/src/PHPNative/Core/src/PathHelper.php @@ -0,0 +1,17 @@ +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) + }; + } +} \ No newline at end of file diff --git a/src/PHPNative/Event/src/Event.php b/src/PHPNative/Event/src/Event.php new file mode 100644 index 0000000..801ba5a --- /dev/null +++ b/src/PHPNative/Event/src/Event.php @@ -0,0 +1,8 @@ +type; + } + +} \ No newline at end of file diff --git a/src/PHPNative/Event/src/MouseMove.php b/src/PHPNative/Event/src/MouseMove.php new file mode 100644 index 0000000..a3f26ff --- /dev/null +++ b/src/PHPNative/Event/src/MouseMove.php @@ -0,0 +1,17 @@ +type; + } + +} \ No newline at end of file diff --git a/src/PHPNative/Event/src/MouseUp.php b/src/PHPNative/Event/src/MouseUp.php new file mode 100644 index 0000000..f0072f4 --- /dev/null +++ b/src/PHPNative/Event/src/MouseUp.php @@ -0,0 +1,17 @@ +type; + } + +} \ No newline at end of file diff --git a/src/PHPNative/Event/src/SystemEvent.php b/src/PHPNative/Event/src/SystemEvent.php new file mode 100644 index 0000000..01eec2f --- /dev/null +++ b/src/PHPNative/Event/src/SystemEvent.php @@ -0,0 +1,16 @@ +type; + } +} \ No newline at end of file diff --git a/src/PHPNative/Framework/composer.json b/src/PHPNative/Framework/composer.json index fd87c82..d01ebb6 100644 --- a/src/PHPNative/Framework/composer.json +++ b/src/PHPNative/Framework/composer.json @@ -6,9 +6,9 @@ }, "autoload": { "psr-4": { - "PHPNative\\Framework\\": "src" - } -}, + "PHPNative\\Framework\\": "src" + } + }, "autoload-dev": { "psr-4": { "PHPNative\\Framework\\Tests\\": "tests" diff --git a/src/PHPNative/Framework/src/App.php b/src/PHPNative/Framework/src/App.php new file mode 100644 index 0000000..7c6686d --- /dev/null +++ b/src/PHPNative/Framework/src/App.php @@ -0,0 +1,10 @@ +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()); + } + } +} \ No newline at end of file diff --git a/src/PHPNative/Framework/src/Application/Kernel.php b/src/PHPNative/Framework/src/Application/Kernel.php index da7c58a..06ebbc9 100644 --- a/src/PHPNative/Framework/src/Application/Kernel.php +++ b/src/PHPNative/Framework/src/Application/Kernel.php @@ -1,47 +1,55 @@ -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 = [ - DiscoveryLocationBootstrap::class, - ConfigBootstrap::class, - DiscoveryBootstrap::class, - ]; + return $this; + } - foreach ($bootstraps as $bootstrap) { - $container->get( - $bootstrap, - kernel: $this, - )->boot(); - } + private function loadDiscovery(): self + { + ($this->container->get(LoadDiscoveryClasses::class))(); - return $container; + return $this; } private function createContainer(): Container @@ -50,11 +58,8 @@ final class Kernel GenericContainer::setInstance($container); - $container - ->singleton(self::class, fn () => $this) - ->singleton(Container::class, fn () => $container) - ; + $container->singleton(Container::class, fn () => $container); + return $container; } - } diff --git a/src/PHPNative/Framework/src/Application/Window.php b/src/PHPNative/Framework/src/Application/Window.php index ec912ea..730fda9 100644 --- a/src/PHPNative/Framework/src/Application/Window.php +++ b/src/PHPNative/Framework/src/Application/Window.php @@ -1,10 +1,13 @@ 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); - } -} diff --git a/src/PHPNative/Framework/src/Discovery/DiscoveryLocation.php b/src/PHPNative/Framework/src/Discovery/DiscoveryLocation.php index b513c2d..ccbb28d 100644 --- a/src/PHPNative/Framework/src/Discovery/DiscoveryLocation.php +++ b/src/PHPNative/Framework/src/Discovery/DiscoveryLocation.php @@ -2,7 +2,7 @@ declare(strict_types=1); -namespace PHPNative\Framework\src\Discovery; +namespace PHPNative\Framework\Discovery; final readonly class DiscoveryLocation { diff --git a/src/PHPNative/Framework/src/Discovery/InitializerDiscovery.php b/src/PHPNative/Framework/src/Discovery/InitializerDiscovery.php index a1b3b00..9bd38bc 100644 --- a/src/PHPNative/Framework/src/Discovery/InitializerDiscovery.php +++ b/src/PHPNative/Framework/src/Discovery/InitializerDiscovery.php @@ -1,5 +1,7 @@ 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(); + } + } +} \ No newline at end of file diff --git a/src/PHPNative/Framework/src/Discovery/LoadDiscoveryLocations.php b/src/PHPNative/Framework/src/Discovery/LoadDiscoveryLocations.php new file mode 100644 index 0000000..c7745eb --- /dev/null +++ b/src/PHPNative/Framework/src/Discovery/LoadDiscoveryLocations.php @@ -0,0 +1,122 @@ +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); + } +} diff --git a/src/PHPNative/Framework/src/Lifecycle/Context.php b/src/PHPNative/Framework/src/Lifecycle/Context.php new file mode 100644 index 0000000..4d636a6 --- /dev/null +++ b/src/PHPNative/Framework/src/Lifecycle/Context.php @@ -0,0 +1,42 @@ +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 + } + +} \ No newline at end of file diff --git a/src/PHPNative/Framework/src/Lifecycle/ContextCollection.php b/src/PHPNative/Framework/src/Lifecycle/ContextCollection.php new file mode 100644 index 0000000..70892ed --- /dev/null +++ b/src/PHPNative/Framework/src/Lifecycle/ContextCollection.php @@ -0,0 +1,15 @@ +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(); + } + } +} \ No newline at end of file diff --git a/src/PHPNative/Framework/src/Loop/EventLoop.php b/src/PHPNative/Framework/src/Loop/EventLoop.php new file mode 100644 index 0000000..0ada423 --- /dev/null +++ b/src/PHPNative/Framework/src/Loop/EventLoop.php @@ -0,0 +1,82 @@ +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); + } + } + +} \ No newline at end of file diff --git a/src/PHPNative/Framework/src/Loop/LoopInterface.php b/src/PHPNative/Framework/src/Loop/LoopInterface.php new file mode 100644 index 0000000..acf4217 --- /dev/null +++ b/src/PHPNative/Framework/src/Loop/LoopInterface.php @@ -0,0 +1,18 @@ +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); + } + } + } +} \ No newline at end of file diff --git a/src/PHPNative/Framework/src/Loop/Timer.php b/src/PHPNative/Framework/src/Loop/Timer.php index 898d671..c92cce4 100644 --- a/src/PHPNative/Framework/src/Loop/Timer.php +++ b/src/PHPNative/Framework/src/Loop/Timer.php @@ -1,7 +1,10 @@ time = $now ?? \microtime(true); + $this->time = $now ?? microtime(true); } - public function rate(int $rate): self { $this->rate = $rate === 0 ? 0 : 1 / $rate; @@ -52,4 +54,4 @@ class Timer return null; } -} \ No newline at end of file +} diff --git a/src/PHPNative/Framework/src/Loop/WorkerInterface.php b/src/PHPNative/Framework/src/Loop/WorkerInterface.php new file mode 100644 index 0000000..ff6db4e --- /dev/null +++ b/src/PHPNative/Framework/src/Loop/WorkerInterface.php @@ -0,0 +1,14 @@ +container; } - } diff --git a/src/PHPNative/Renderer/composer.json b/src/PHPNative/Renderer/composer.json new file mode 100644 index 0000000..e2ecc1d --- /dev/null +++ b/src/PHPNative/Renderer/composer.json @@ -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" + } +} +} \ No newline at end of file diff --git a/src/PHPNative/Renderer/src/Thread.php b/src/PHPNative/Renderer/src/Thread.php new file mode 100644 index 0000000..f8f52e9 --- /dev/null +++ b/src/PHPNative/Renderer/src/Thread.php @@ -0,0 +1,84 @@ +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; + } +} \ No newline at end of file diff --git a/src/PHPNative/Renderer/src/Viewport.php b/src/PHPNative/Renderer/src/Viewport.php new file mode 100644 index 0000000..aa0eca3 --- /dev/null +++ b/src/PHPNative/Renderer/src/Viewport.php @@ -0,0 +1,25 @@ +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; + } +} \ No newline at end of file diff --git a/src/PHPNative/Renderer/src/Widgets/Button.php b/src/PHPNative/Renderer/src/Widgets/Button.php new file mode 100644 index 0000000..ef4c283 --- /dev/null +++ b/src/PHPNative/Renderer/src/Widgets/Button.php @@ -0,0 +1,90 @@ +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; + } + +} \ No newline at end of file diff --git a/src/PHPNative/Renderer/src/Widgets/Container.php b/src/PHPNative/Renderer/src/Widgets/Container.php new file mode 100644 index 0000000..8b23c7f --- /dev/null +++ b/src/PHPNative/Renderer/src/Widgets/Container.php @@ -0,0 +1,46 @@ +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; + } +} \ No newline at end of file diff --git a/src/PHPNative/Support/composer.json b/src/PHPNative/Support/composer.json new file mode 100644 index 0000000..fa128eb --- /dev/null +++ b/src/PHPNative/Support/composer.json @@ -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" + } +} +} \ No newline at end of file diff --git a/src/PHPNative/Support/src/Reflection/ClassReflector.php b/src/PHPNative/Support/src/Reflection/ClassReflector.php new file mode 100644 index 0000000..4781dab --- /dev/null +++ b/src/PHPNative/Support/src/Reflection/ClassReflector.php @@ -0,0 +1,126 @@ +|object|PHPReflectionClass $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 */ + public function getPublicProperties(): Generator + { + foreach ($this->reflectionClass->getProperties(PHPReflectionProperty::IS_PUBLIC) as $property) { + yield new PropertyReflector($property); + } + } + + /** @return Generator */ + 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 + */ + 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); + } +} \ No newline at end of file diff --git a/src/PHPNative/Support/src/Reflection/FunctionReflector.php b/src/PHPNative/Support/src/Reflection/FunctionReflector.php new file mode 100644 index 0000000..4289820 --- /dev/null +++ b/src/PHPNative/Support/src/Reflection/FunctionReflector.php @@ -0,0 +1,50 @@ +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(); + } +} diff --git a/src/PHPNative/Support/src/Reflection/HasAttributes.php b/src/PHPNative/Support/src/Reflection/HasAttributes.php new file mode 100644 index 0000000..aaaa49d --- /dev/null +++ b/src/PHPNative/Support/src/Reflection/HasAttributes.php @@ -0,0 +1,46 @@ +getReflection()->getAttributes($name) !== []; + } + + /** + * @template TAttributeClass of object + * @param class-string $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 $attributeClass + * @return TAttributeClass[] + */ + public function getAttributes(string $attributeClass): array + { + return array_map( + fn (PHPReflectionAttribute $attribute) => $attribute->newInstance(), + $this->getReflection()->getAttributes($attributeClass, PHPReflectionAttribute::IS_INSTANCEOF) + ); + } +} diff --git a/src/PHPNative/Support/src/Reflection/MethodReflector.php b/src/PHPNative/Support/src/Reflection/MethodReflector.php new file mode 100644 index 0000000..f7b4a17 --- /dev/null +++ b/src/PHPNative/Support/src/Reflection/MethodReflector.php @@ -0,0 +1,87 @@ +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'], + ); + } +} diff --git a/src/PHPNative/Support/src/Reflection/ParameterReflector.php b/src/PHPNative/Support/src/Reflection/ParameterReflector.php new file mode 100644 index 0000000..15c471d --- /dev/null +++ b/src/PHPNative/Support/src/Reflection/ParameterReflector.php @@ -0,0 +1,73 @@ +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(); + } +} diff --git a/src/PHPNative/Support/src/Reflection/PropertyReflector.php b/src/PHPNative/Support/src/Reflection/PropertyReflector.php new file mode 100644 index 0000000..884f657 --- /dev/null +++ b/src/PHPNative/Support/src/Reflection/PropertyReflector.php @@ -0,0 +1,130 @@ +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; + } +} diff --git a/src/PHPNative/Support/src/Reflection/Reflector.php b/src/PHPNative/Support/src/Reflection/Reflector.php new file mode 100644 index 0000000..be12f35 --- /dev/null +++ b/src/PHPNative/Support/src/Reflection/Reflector.php @@ -0,0 +1,10 @@ +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'); + } +} diff --git a/src/PHPNative/Tailwind/src/Model/Style.php b/src/PHPNative/Tailwind/src/Model/Style.php new file mode 100644 index 0000000..fa1cdec --- /dev/null +++ b/src/PHPNative/Tailwind/src/Model/Style.php @@ -0,0 +1,17 @@ +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; + } +} \ No newline at end of file diff --git a/src/PHPNative/Tailwind/src/Parser/Background.php b/src/PHPNative/Tailwind/src/Parser/Background.php index c3efa88..90acab6 100644 --- a/src/PHPNative/Tailwind/src/Parser/Background.php +++ b/src/PHPNative/Tailwind/src/Parser/Background.php @@ -1,21 +1,22 @@ 0) { + if (count($output_array[0]) > 0) { $colorStyle = $output_array[1][0]; $color = Color::parse($colorStyle); } + return new \PHPNative\Tailwind\Style\Background($color); } -} \ No newline at end of file +} diff --git a/src/PHPNative/Tailwind/src/Parser/Color.php b/src/PHPNative/Tailwind/src/Parser/Color.php index 87906e6..f1d7bf3 100644 --- a/src/PHPNative/Tailwind/src/Parser/Color.php +++ b/src/PHPNative/Tailwind/src/Parser/Color.php @@ -1,11 +1,11 @@ 0) { + if (count($output_array[0]) > 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); - if(count($output_array[0]) > 0) { + if (count($output_array[0]) > 0) { $color = (string)$output_array[1][0]; $variant = (string)$output_array[2][0]; - list($red, $green, $blue) = sscanf($data[$color][$variant], "#%02x%02x%02x"); + [$red, $green, $blue] = sscanf($data[$color][$variant], "#%02x%02x%02x"); } return new \PHPNative\Tailwind\Style\Color($red, $green, $blue); } -} \ No newline at end of file +} diff --git a/src/PHPNative/Tailwind/src/Parser/Margin.php b/src/PHPNative/Tailwind/src/Parser/Margin.php new file mode 100644 index 0000000..6ebbb68 --- /dev/null +++ b/src/PHPNative/Tailwind/src/Parser/Margin.php @@ -0,0 +1,78 @@ + 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; + } + } +} diff --git a/src/PHPNative/Tailwind/src/Parser/MediaQuery.php b/src/PHPNative/Tailwind/src/Parser/MediaQuery.php new file mode 100644 index 0000000..31f0c77 --- /dev/null +++ b/src/PHPNative/Tailwind/src/Parser/MediaQuery.php @@ -0,0 +1,18 @@ + 0) { + $query = strtolower(strrev($output_array[1][0])); + return \PHPNative\Tailwind\Style\MediaQueryEnum::{$query}; + } + + return null; + } +} \ No newline at end of file diff --git a/src/PHPNative/Tailwind/src/Parser/Padding.php b/src/PHPNative/Tailwind/src/Parser/Padding.php index 5941226..ccb537a 100644 --- a/src/PHPNative/Tailwind/src/Parser/Padding.php +++ b/src/PHPNative/Tailwind/src/Parser/Padding.php @@ -1,52 +1,78 @@ 0) { + preg_match_all('/p-(\d*)/', $style, $output_array); + if (count($output_array[0]) > 0) { $l = (int)$output_array[1][0]; $r = (int)$output_array[1][0]; $t = (int)$output_array[1][0]; $b = (int)$output_array[1][0]; } - preg_match_all('/px-(\d)/', $style, $output_array); - if(count($output_array[0]) > 0) { + + preg_match_all('/px-(\d*)/', $style, $output_array); + if (count($output_array[0]) > 0) { $l = (int)$output_array[1][0]; $r = (int)$output_array[1][0]; } - preg_match_all('/py-(\d)/', $style, $output_array); - if(count($output_array[0]) > 0) { - $t = (int)$output_array[1][0]; - $b = (int)$output_array[1][0]; - } - preg_match_all('/pt-(\d)/', $style, $output_array); - if(count($output_array[0]) > 0) { - $t = (int)$output_array[1][0]; - } - preg_match_all('/pb-(\d)/', $style, $output_array); - if(count($output_array[0]) > 0) { - $b = (int)$output_array[1][0]; - } - preg_match_all('/pl-(\d)/', $style, $output_array); - if(count($output_array[0]) > 0) { - $l = (int)$output_array[1][0]; - } - preg_match_all('/pr-(\d)/', $style, $output_array); - if(count($output_array[0]) > 0) { - $r = (int)$output_array[1][0]; - } - return new \PHPNative\Tailwind\Style\Padding($l, $r, $t, $b); + 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; } -} \ No newline at end of file + + 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; + } + } +} diff --git a/src/PHPNative/Tailwind/src/Parser/Parser.php b/src/PHPNative/Tailwind/src/Parser/Parser.php index 982dd1f..e47bcd7 100644 --- a/src/PHPNative/Tailwind/src/Parser/Parser.php +++ b/src/PHPNative/Tailwind/src/Parser/Parser.php @@ -1,4 +1,5 @@ 0) { + $query = strtolower($output_array[1][0]); + return \PHPNative\Tailwind\Style\StateEnum::{$query}; + } + + return null; + } +} \ No newline at end of file diff --git a/src/PHPNative/Tailwind/src/Parser/Width.php b/src/PHPNative/Tailwind/src/Parser/Width.php new file mode 100644 index 0000000..b8f89e0 --- /dev/null +++ b/src/PHPNative/Tailwind/src/Parser/Width.php @@ -0,0 +1,60 @@ + 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; + } + } +} diff --git a/src/PHPNative/Tailwind/src/Style/Background.php b/src/PHPNative/Tailwind/src/Style/Background.php index d2f5ff8..df87c45 100644 --- a/src/PHPNative/Tailwind/src/Style/Background.php +++ b/src/PHPNative/Tailwind/src/Style/Background.php @@ -1,13 +1,12 @@ 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; + } +} \ No newline at end of file diff --git a/src/PHPNative/Tailwind/src/Style/Padding.php b/src/PHPNative/Tailwind/src/Style/Padding.php index a9d212e..ad581f9 100644 --- a/src/PHPNative/Tailwind/src/Style/Padding.php +++ b/src/PHPNative/Tailwind/src/Style/Padding.php @@ -1,11 +1,12 @@ mediaQuery = $mq; + } + $state = \PHPNative\Tailwind\Parser\State::parse($styleStr); + if($state) { + $s->state = $state; + } + $computed->add($s); + } + return $computed; } -} \ No newline at end of file + 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; + } +} diff --git a/src/PHPNative/Tailwind/tests/BackgroundTest.php b/src/PHPNative/Tailwind/tests/BackgroundTest.php index 8bd9c91..b9e143c 100644 --- a/src/PHPNative/Tailwind/tests/BackgroundTest.php +++ b/src/PHPNative/Tailwind/tests/BackgroundTest.php @@ -1,4 +1,5 @@ assertInstanceOf(\PHPNative\Tailwind\Style\Background::class, $bg); @@ -18,5 +22,4 @@ class BackgroundTest extends TestCase $this->assertSame(225, $bg->color->blue); $this->assertSame(0, $bg->color->alpha); } - -} \ No newline at end of file +} diff --git a/src/PHPNative/Tailwind/tests/ColorTest.php b/src/PHPNative/Tailwind/tests/ColorTest.php index 10c0c51..16d7446 100644 --- a/src/PHPNative/Tailwind/tests/ColorTest.php +++ b/src/PHPNative/Tailwind/tests/ColorTest.php @@ -1,4 +1,5 @@ red); - self::assertSame(68, $color->green); - self::assertSame(68, $color->blue); - self::assertSame(0, $color->alpha); + $this->assertInstanceOf(\PHPNative\Tailwind\Style\Color::class, $color); + $this->assertSame(239, $color->red); + $this->assertSame(68, $color->green); + $this->assertSame(68, $color->blue); + $this->assertSame(0, $color->alpha); } - public function testNameVariant(): void + public function test_name_variant(): void { $color = Color::parse("lime-300"); - self::assertInstanceOf(\PHPNative\Tailwind\Style\Color::class, $color); - self::assertSame(190, $color->red); - self::assertSame(242, $color->green); - self::assertSame(100, $color->blue); - self::assertSame(0, $color->alpha); + $this->assertInstanceOf(\PHPNative\Tailwind\Style\Color::class, $color); + $this->assertSame(190, $color->red); + $this->assertSame(242, $color->green); + $this->assertSame(100, $color->blue); + $this->assertSame(0, $color->alpha); } -} \ No newline at end of file + + 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); + } +} diff --git a/src/PHPNative/Tailwind/tests/ComplexTest.php b/src/PHPNative/Tailwind/tests/ComplexTest.php new file mode 100644 index 0000000..a21a978 --- /dev/null +++ b/src/PHPNative/Tailwind/tests/ComplexTest.php @@ -0,0 +1,34 @@ +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)); + } +} \ No newline at end of file diff --git a/src/PHPNative/Tailwind/tests/MarginTest.php b/src/PHPNative/Tailwind/tests/MarginTest.php new file mode 100644 index 0000000..57bb60b --- /dev/null +++ b/src/PHPNative/Tailwind/tests/MarginTest.php @@ -0,0 +1,95 @@ +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); + } +} diff --git a/src/PHPNative/Tailwind/tests/PaddingTest.php b/src/PHPNative/Tailwind/tests/PaddingTest.php index 513bb5c..120e383 100644 --- a/src/PHPNative/Tailwind/tests/PaddingTest.php +++ b/src/PHPNative/Tailwind/tests/PaddingTest.php @@ -1,5 +1,7 @@ left); - self::assertSame(6, $padding->right); - self::assertSame(6, $padding->top); - self::assertSame(6, $padding->bottom); + $this->assertInstanceOf(\PHPNative\Tailwind\Style\Padding::class, $padding); + $this->assertSame(6, $padding->left); + $this->assertSame(6, $padding->right); + $this->assertSame(6, $padding->top); + $this->assertSame(6, $padding->bottom); } - public function testPaddingX(): void + public function test_padding_x(): void { $padding = Padding::parse("px-2"); - self::assertInstanceOf(\PHPNative\Tailwind\Style\Padding::class, $padding); - self::assertSame(2, $padding->left); - self::assertSame(2, $padding->right); - self::assertSame(0, $padding->top); - self::assertSame(0, $padding->bottom); + $this->assertInstanceOf(\PHPNative\Tailwind\Style\Padding::class, $padding); + $this->assertSame(2, $padding->left); + $this->assertSame(2, $padding->right); + $this->assertSame(null, $padding->top); + $this->assertSame(null, $padding->bottom); } - public function testPaddingY(): void + public function test_padding_y(): void { $padding = Padding::parse("py-2"); - self::assertInstanceOf(\PHPNative\Tailwind\Style\Padding::class, $padding); - self::assertSame(0, $padding->left); - self::assertSame(0, $padding->right); - self::assertSame(2, $padding->top); - self::assertSame(2, $padding->bottom); + $this->assertInstanceOf(\PHPNative\Tailwind\Style\Padding::class, $padding); + $this->assertSame(null, $padding->left); + $this->assertSame(null, $padding->right); + $this->assertSame(2, $padding->top); + $this->assertSame(2, $padding->bottom); } - public function testPaddingT(): void + public function test_padding_t(): void { $padding = Padding::parse("pt-3"); - self::assertInstanceOf(\PHPNative\Tailwind\Style\Padding::class, $padding); - self::assertSame(0, $padding->left); - self::assertSame(0, $padding->right); - self::assertSame(3, $padding->top); - self::assertSame(0, $padding->bottom); + $this->assertInstanceOf(\PHPNative\Tailwind\Style\Padding::class, $padding); + $this->assertSame(null, $padding->left); + $this->assertSame(null, $padding->right); + $this->assertSame(3, $padding->top); + $this->assertSame(null, $padding->bottom); } - public function testPaddingB(): void + public function test_padding_b(): void { $padding = Padding::parse("pb-3"); - self::assertInstanceOf(\PHPNative\Tailwind\Style\Padding::class, $padding); - self::assertSame(0, $padding->left); - self::assertSame(0, $padding->right); - self::assertSame(0, $padding->top); - self::assertSame(3, $padding->bottom); + $this->assertInstanceOf(\PHPNative\Tailwind\Style\Padding::class, $padding); + $this->assertSame(null, $padding->left); + $this->assertSame(null, $padding->right); + $this->assertSame(null, $padding->top); + $this->assertSame(3, $padding->bottom); } - public function testPaddingL(): void + public function test_padding_l(): void { $padding = Padding::parse("pl-3"); - self::assertInstanceOf(\PHPNative\Tailwind\Style\Padding::class, $padding); - self::assertSame(3, $padding->left); - self::assertSame(0, $padding->right); - self::assertSame(0, $padding->top); - self::assertSame(0, $padding->bottom); + $this->assertInstanceOf(\PHPNative\Tailwind\Style\Padding::class, $padding); + $this->assertSame(3, $padding->left); + $this->assertSame(null, $padding->right); + $this->assertSame(null, $padding->top); + $this->assertSame(null, $padding->bottom); } - public function testPaddingR(): void + public function test_padding_r(): void { $padding = Padding::parse("pr-3"); - self::assertInstanceOf(\PHPNative\Tailwind\Style\Padding::class, $padding); - self::assertSame(0, $padding->left); - self::assertSame(3, $padding->right); - self::assertSame(0, $padding->top); - self::assertSame(0, $padding->bottom); + $this->assertInstanceOf(\PHPNative\Tailwind\Style\Padding::class, $padding); + $this->assertSame(null, $padding->left); + $this->assertSame(3, $padding->right); + $this->assertSame(null, $padding->top); + $this->assertSame(null, $padding->bottom); } - public function testPaddingComplex(): void + public function test_padding_complex(): void { $padding = Padding::parse("p-2 pl-1 pr-3"); - self::assertInstanceOf(\PHPNative\Tailwind\Style\Padding::class, $padding); - self::assertSame(1, $padding->left); - self::assertSame(3, $padding->right); - self::assertSame(2, $padding->top); - self::assertSame(2, $padding->bottom); + $this->assertInstanceOf(\PHPNative\Tailwind\Style\Padding::class, $padding); + $this->assertSame(1, $padding->left); + $this->assertSame(3, $padding->right); + $this->assertSame(2, $padding->top); + $this->assertSame(2, $padding->bottom); } -} \ No newline at end of file +} diff --git a/src/PHPNative/Tailwind/tests/WidthTest.php b/src/PHPNative/Tailwind/tests/WidthTest.php new file mode 100644 index 0000000..ae30619 --- /dev/null +++ b/src/PHPNative/Tailwind/tests/WidthTest.php @@ -0,0 +1,44 @@ +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); + } +} diff --git a/src/PHPNative/UI/View.php b/src/PHPNative/UI/View.php deleted file mode 100644 index b35e317..0000000 --- a/src/PHPNative/UI/View.php +++ /dev/null @@ -1,8 +0,0 @@ -view; + } +} \ No newline at end of file diff --git a/src/PHPNative/UI/src/Trait/Action/Click.php b/src/PHPNative/UI/src/Trait/Action/Click.php index 0ad91d1..f271db5 100644 --- a/src/PHPNative/UI/src/Trait/Action/Click.php +++ b/src/PHPNative/UI/src/Trait/Action/Click.php @@ -1,10 +1,26 @@ click = $onClick; + return $this; + } + + public function onClick(): void + { + if($this->click instanceof Closure) { + ($this->click)(); + } + + } +} diff --git a/src/PHPNative/UI/src/Trait/State.php b/src/PHPNative/UI/src/Trait/State.php new file mode 100644 index 0000000..6d5eafc --- /dev/null +++ b/src/PHPNative/UI/src/Trait/State.php @@ -0,0 +1,13 @@ +