slotDefinitions(); $depth++; return ""; case "/block": case "endblock": $depth--; return "render(\$___class__template); unset(\$___block); ?>"; case "arguments": return <<using ?? [], \EXTR_OVERWRITE); extract( \\$class::parseArguments(function($arguments) {}, \$inlineVariables), \EXTR_OVERWRITE); unset(\$inlineVariables); } catch(\TypeError \$ex) { \$showArgs = fn(\$e) => (\$type = gettype(\$e)) === 'object' ? \$e::class : \$type; throw new \Exception( sprintf('A block awaiting arguments (%s) instead received (%s) with values (%s)', '$arguments', implode(', ', array_map(\$showArgs, \$inlineVariables ?? [])), json_encode(\$inlineVariables)) ); } /*%EXCEPTION_LINE_BASE%*/?> PHP; case "define": list($name, $definition) = array_pad(explode(',', $arguments, 2), 2, ""); ( $slotDefinitions[$depth] ?? end($slotDefinitions) )->setDefinition(eval("return $name;"), $definition); return <<defineSlot($name, function($definition) {}); ?> PHP; case "slot": $def = ( $slotDefinitions[$depth] ?? end($slotDefinitions) ); list($name, $definition) = array_pad(explode(',', $arguments, 2), 2, ""); $loops = count($context->iterationStack ?? []) ? ",". implode(', ', array_filter(array_column($context->iterationStack, 'uid'), fn($e) => strpos($e, '[') === false)) : null; $slotDepth++; if ($def->hasDefinitions() ) { $slotName[] = eval("return $name;"); $def->currentSlot = end($slotName); $def->setDefinitionVars(end($slotName), $definition); $definition = $def->printDefinition(end($slotName)); if ($definition) { $definition .= ","; } return <<printSlot($name, function($definition array \$___using = []) use (\$picea, \$___class__template $loops) { extract(\$___using, \EXTR_SKIP); ?> PHP; } else { if ($definition) { $definition .= ","; } return <<slotIsSet($name) || (\$___block ?? \$this)->setSlot($name, function($definition array \$___using = []) use (\$picea, \$___class__template $loops) { extract(\$___using, \EXTR_SKIP); ?> PHP; } case "/slot": case "endslot": $slotDepth--; $def = ( $slotDefinitions[$depth] ?? end($slotDefinitions) ); if ($def->hasDefinitions() ) { $def->currentSlot = array_pop($slotName); $definition = $def->getCurrentSlotDefinitionVars(); if ($definition) { $definition .= ","; } return <<call(\$this, $definition array_merge(get_defined_vars(), \$this->using)); ?> PHP; } else { return ""; } case "public": case "private": case "protected": $arguments = rtrim($arguments, ';'); return "createProperty(\\$class::parseProperty('$token', fn($arguments) => func_get_arg(0))) ?>"; case "set": return "setPropertyValue({$arguments}); ?>"; # DEPRECATED ; should be removed, to be superseded by 'set' instead case "using": return "setUsing($arguments); ?>"; } throw new \LogicException("Unhandled token ? Should never have been here !"); } public function inlineHtml(? object $proxy, string $viewPath, ... $variables) { return $this->renderHtml($viewPath, [ 'inlineVariables' => $variables ], $proxy); } public static function parseSlotArguments(Callable $method) : array { #return } public static function parseArguments(Callable $method, array $arguments) : array { try{ call_user_func_array($method, $arguments); } catch(\TypeError $ex) { throw $ex; } $parameters = []; foreach((new \ReflectionFunction($method))->getParameters() as $key => $value) { $name = $value->getName(); # Ordered arguments if ( isset($arguments[$key]) ) { $parameters[ $name ] = $arguments[$key]; } # Named arguments elseif (isset($arguments[$name])) { $parameters[ $name ] = $arguments[$name]; } # Using default value elseif ( $value->isDefaultValueAvailable() ) { $parameters[ $name ] = $value->getDefaultValue(); } elseif ( $value->isVariadic() ) { $parameters[ $name ] = []; } else { $parameters[ $name ] = null; } } return $parameters; } public static function parseProperty(string $token, \Closure $method) : BlockToken\Property { foreach((new \ReflectionFunction($method))->getParameters() as $value) { $property = new BlockToken\Property( name: $value->getName(), scope: BlockToken\PropertyScopeEnum::from($token), setter: $method, ); if ($value->isDefaultValueAvailable()) { $property->value = $value->getDefaultValue(); } # For now, we only allow one declaration per token, might be revised at another moment... break; } return $property ?? throw new \LogicException("No parameters found in property declaration."); } public static function slotDefinitions() { return new class() { public array $definitions = []; public array $variables = []; public bool $rendering = false; public string $currentSlot; public function setDefinition(string $name, string $definition) : self { $this->definitions[$name] = $definition; return $this; } public function setDefinitionVars(string $name, string $variables) : self { $this->variables[$name] = $variables; return $this; } public function getDefinitionVars(string $name) : string { return $this->variables[$name] ?? ""; } public function getCurrentSlotDefinitionVars() : string { return $this->getDefinitionVars($this->currentSlot); } public function hasDefinitions() : bool { return count($this->definitions) > 0; } public function printDefinition(string $name) : string { if ( ! isset($this->definitions[$name]) ) { throw new \Exception("Slot definition for `$name` was not found. Have you defined it in your block header using something like '{% define \"$name\", ...\$arguments %}' ?"); } return $this->definitions[$name]; } public function render() : void { $this->rendering = true; } }; } public static function instanciateBlock(string $viewPath, ... $arguments) { return new class($viewPath, ...$arguments) { public bool $rendering = false; public string $viewPath; public array $using = []; public array $arguments = []; public array $slots = []; public array $definition = []; protected array $properties = []; private array $definedPropertyValues = []; public function __construct(string $viewPath, ...$arguments) { $this->viewPath = $viewPath; $this->arguments = $arguments; } public function render(object $classTemplate) : string { $this->rendering = true; if ($this->using['this'] ?? false) { throw new \InvalidArgumentException("Block '{$this->viewPath}' cannot overwrite 'this' variable."); } return $classTemplate->picea->inlineBlock($this, $this->viewPath, ...$this->arguments); } public function setSlot(string $name, Callable $method) : void { $this->slots[$name] = $method; } public function defineSlot(string $name, Callable $method) : void { $this->definition[$name] = $method; } public function slotIsSet(string $name) : bool { return ! empty($this->slots[$name]); } public function printSlot(string $name, Callable $default) : mixed { return $this->slotIsSet($name) ? $this->slots[$name] : $default; } public function setUsing(array $variables) : void { $this->using = $variables; } public function setPropertyValue(array $vars) : void { foreach($vars as $key => $value) { $this->definedPropertyValues[$key] = $value; } } public function createProperty(BlockToken\Property $property) : void { if (isset($this->properties[$property->name])) { throw new \InvalidArgumentException("Given property '{$property->name}' is already defined somewhere else in this block."); } $this->properties[$property->name] = $property; if (isset($this->definedPropertyValues[$property->name])) { if ($property->scope !== BlockToken\PropertyScopeEnum::Public) { throw new \InvalidArgumentException( sprintf("Trying to set property %s but it's scope (%s) makes it impossible", $property->name, $property->scope->name) ); } $this->__set($property->name, $this->definedPropertyValues[$property->name]); } } public function __set(string $name, mixed $value) : void { $this->properties[$name]->value = call_user_func($this->properties[$name]->setter, $value); } public function __get(string $name) : mixed { return $this->properties[$name]->value; } }; } }