getProperties() as $property) { if (! $property->isPublic() || ($property->getDeclaringClass()->getShortName() === 'Component')) { $this->protectedSettings[] = $property->getName(); } } $this->setSettings($settings); if (! $this->language) { $this->language = Lang::current(); } $this->init(); $this->blocks(); $this->app->hook('view.block.init', $this); } /** * Set block template * @param string|callable $template * @param Module|string|null $controller * @return static */ public function setTemplate($template, $controller = null) { $this->template = $template; if ($controller) { $this->setController($controller); } return $this; } /** * Get block template * @return string|callable */ public function getTemplate() { return $this->template; } /** * Set block controller * @param Module|string $controller * @return static */ public function setController($controller) { $this->controller = $controller; return $this; } /** * Get block controller * @return Module|string */ public function getController() { return $this->controller ?: $this->app->guessControllerByClassName(static::class); } /** * Set parent block * @param Block|mixed $parent * @return static */ public function setParent($parent) { $this->parent = $parent; return $this; } /** * Get parent block * @return Block|mixed */ public function getParent() { return $this->parent; } /** * Get parent Page * @return Page|null */ public function getParentPage() { if ($this instanceof Page) { return $this; } $parent = $this->getParent(); if ($parent) { return $parent->getParentPage(); } return null; } /** * Block unique id * @return string */ public function id(): string { return $this->blockId ?? '1'; } /** * Set block id * @param string $id * @return static */ public function setId(string $id) { $this->blockId = $id; return $this; } /** * Set block key * @param string $key * @return void */ public function setKey(string $key) { $this->blockKey = $key; } /** * Block unique key * @return string */ public function getKey(): string { return ($this->blockKey ?? (new ReflectionClass($this))->getShortName()); } /** * Block title * @return string */ public function getTitle(): string { return ($this->blockTitle ?? $this->getKey()); } /** * Set block title * @param string $title * @return static */ public function setTitle(string $title) { $this->blockTitle = $title; return $this; } /** * Set block settings (all public properties except "protected" list) + data (fallback) * @param array|string $keys * @param mixed $value * @param array $opts * @return void */ public function setSettings($keys, $value = false, array $opts = []) { if (! is_array($keys)) { $keys = [$keys => $value]; } foreach ($keys as $key => $val) { if (property_exists($this, $key) && ! $this->isProtectedSetting($key)) { $this->$key = $val; } else { $this->data[$key] = $val; } } } /** * Is block setting protected * @param string $key * @return bool true - protected */ public function isProtectedSetting($key) { return in_array($key, $this->protectedSettings, true); } /** * Set block data * @param string|array $key * @param mixed $value * @return static */ public function with($key, $value = null) { if (! is_array($this->data)) { $this->log('Failed to set block data using "with" method'); return $this; } if (is_array($key)) { $this->data = array_merge($this->data, $key); } else { $this->data[$key] = $value; } return $this; } /** * Add sub blocks here * @return void */ public function blocks() { } /** * Add sub block * @param string $key * @param string|Block|Closure $block * @param Closure|null $callback * @return static */ public function addBlock(string $key, $block, $callback = null) { if ($block instanceof self) { $block->setParent($this); if ($callback) { $callback($block, $key); } $this->blocks[$key] = $block; return $this; } $this->blocks[$key] = function () use ($key, $block, $callback) { if ($block instanceof Closure) { $block = $block(); } if (is_string($block) && class_exists($block)) { $block = $this->app->make($block); } if ($block instanceof self) { $this->addBlock($key, $block, $callback); return $block; } return null; }; return $this; } /** * Add template block * @param string $key * @param string $template * @param Module|string|null $controller * @param Closure|null $callback * @return static */ public function addTemplateBlock(string $key, string $template, $controller = null, $callback = null) { $render = function () use ($template, $controller) { return $this->view->template( $template, $this->data, $controller, $this->renderOptions ); }; return $this->addBlock($key, function () use ($render) { return $this->app->make(self::class)->setTemplate($render)->prerenderable(true); }, $callback); } /** * Block(s) to skip * @param string|array $key * @param bool $reset * @return static */ public function withoutBlock($key, bool $reset = false) { if ($reset) { $this->withoutBlocks = []; } if (is_array($key)) { foreach ($key as $k) { $this->withoutBlocks[] = $k; } } else { $this->withoutBlocks[] = $key; } return $this; } /** * Get sub block by key * @param string $key * @param array $opts * @return Block|null */ public function getBlock(string $key, array $opts = []) { if (! isset($this->blocks[$key])) { return null; } if (is_callable($this->blocks[$key])) { $this->blocks[$key] = $this->blocks[$key](); } if ($this->withoutBlocks && $this->blocks[$key] instanceof self) { $this->blocks[$key]->withoutBlock($this->withoutBlocks); } $this->blocks[$key]->fillSettings(); return $this->blocks[$key]; } /** * Set sub block settings and return block instance * @param string $key * @param array $settings * @return Block|null */ public function setBlockSettings(string $key, array $settings) { $block = $this->getBlock($key); if ($block) { $block->setSettings($settings); return $block; } return null; } /** * Has sub blocks * @return bool */ public function hasBlocks() { return ! empty($this->blocks); } /** * Iterate sub blocks * @param Closure $callback * @param array $opts * @return void */ public function blocksIterator(Closure $callback, array $opts = []) { foreach ($this->blocks as $key => $block) { if (in_array($key, $this->withoutBlocks, true)) { continue; } $block = $this->getBlock($key, $opts); if ($block instanceof self) { if ($callback($block, $key) === false) { break; } } } } /** * Add content wrapper * @param callable|string $template * @param string $controller * @param array $opts * @return static */ public function addWrapper($template, $controller = null, array $opts = []) { $opts = $this->defaults($opts, [ 'contentKey' => 'content', ]); $opts['template'] = $template; $opts['controller'] = $controller; $this->wrappers[] = $opts; return $this; } /** * Handle action request. * @param string|null $action * @return mixed|null */ public function handleActionRequest(?string $action = null) { $action = $action ?: $this->getRequestAction(); if (empty($action)) { return null; } $actionMethod = 'on' . Str::studly($action) . 'Action'; if (method_exists($this, $actionMethod)) { if ($response = $this->app->call([$this, $actionMethod])) { return $response; } } $response = null; $this->blocksIterator(function (self $block) use ($action, &$response) { if ($response = $block->handleActionRequest($action)) { return false; } return true; }); return $response; } /** * Before render * @return bool|void */ protected function beforeRender() { $this->beforeRenderRotation(); } /** * Render block * @return string|mixed */ public function render() { $content = $this->renderContent(); if (! is_string($content)) { return $content; } if (! empty($this->wrappers)) { foreach (array_reverse($this->wrappers) as $wrapper) { if (empty($wrapper['template'])) { continue; } if (is_string($wrapper['template']) && is_string($content)) { $data = $this->data; if (! is_array($data)) { $data = []; } $data[$wrapper['contentKey'] ?? 'content'] = $content; $content = $this->view->template( $wrapper['template'], $data, $wrapper['controller'] ?? $this->getController(), $wrapper['renderOptions'] ?? $this->renderOptions ); continue; } if (is_callable($wrapper['template'])) { $content = call_user_func($wrapper['template'], $content, $this->data); } } } return $this->app->filter('view.block.render', $content, $this); } /** * Render block content * @return string|mixed */ protected function renderContent() { $this->gatherData(); # Try to return data if (! is_array($this->data)) { if ($this->data instanceof self) { return $this->data->render(); } # cancel render if ($this->beforeRender() === false) { return ''; } # string is a render goal if (is_string($this->data)) { return $this->data; } # throw response if ($this->data instanceof Response) { $this->data->throw(); } if ($this->data instanceof Closure) { $callback = $this->data; $data = $callback($this); # throw response if ($data instanceof Response) { $data->throw(); } return $data; } return ''; # no data no render } if ($this->beforeRender() === false) { return ''; # cancel render } # No template - no render $template = $this->getTemplate(); if ($template === '') { return ''; } # Prerender sub blocks foreach ($this->data as $key => $value) { if ($value instanceof self && $value->prerenderable()) { $this->data[$key] = $value->render(); } } # Bind scope $this->renderOptions['this'] = $this->renderOptions['this'] ?? $this; # Template as callback if ($template instanceof Closure) { return $template($this->data, $this); } return $this->view->template( $template, $this->data, $this->getController(), $this->renderOptions ); } /** * Set/get prerenderable status * @param bool|callable|null $prerenderable * @return static|bool|mixed */ public function prerenderable($prerenderable = null) { if (is_null($prerenderable)) { if (is_callable($this->prerenderable)) { return ($this->prerenderable)(); } return $this->prerenderable; } $this->prerenderable = $prerenderable; return $this; } /** * Set render options * @param array $options * @param bool $merge * @return static */ public function setRenderOptions(array $options, $merge = true) { $this->renderOptions = ($merge ? array_merge($this->renderOptions, $options) : $options); return $this; } /** * Prepare data before render * @return array|bool */ public function data() { return $this->data; } /** * Fill data with settings * @return void */ protected function settingsToData() { if (! is_array($this->data)) { return; } foreach (array_keys(get_class_vars(static::class)) as $key) { # Skip protected properties if ($this->isProtectedSetting($key)) { continue; } # Do not override data keys if (array_key_exists($key, $this->data)) { continue; } $this->data[$key] = &$this->$key; } } /** * Gather data before render * @return void */ protected function gatherData() { $this->fillSettings(); $this->settingsToData(); $this->data = $this->data(); $this->app->hook('view.block.data', $this, ['data' => & $this->data]); if (is_array($this->data)) { $this->blocksIterator(function ($block, $key) { $this->data[$key] = $block; }); } } /** * Get block (and sub blocks) data without rendering * @return array|mixed */ public function getData() { $this->gatherData(); if (is_array($this->data)) { foreach ($this->data as $key => $value) { if ($value instanceof self) { $this->data[$key] = $value->getData(); } } } return $this->data; } /** * Render block content * @return string|mixed */ public function __toString() { return $this->render(); } /** * Json encode to string * @return string|mixed */ public function jsonSerialize() { return $this->render(); } /** * Set settings or data * @param string $key * @param mixed $value * @return void */ public function __set($key, $value) { $this->setSettings($key, $value); } /** * Get data * @param string $key * @return mixed */ public function &__get($key) { if (! array_key_exists($key, $this->data)) { $this->data[$key] = null; } return $this->data[$key]; } /** * Check data * @param string $key * @return bool */ public function __isset($key) { return isset($this->data[$key]); } /** * Unset data * @param string $key * @return void */ public function __unset($key) { unset($this->data[$key]); } /** * Is desktop device * @return bool */ public function isDesktop() { return $this->request->isDesktop(); } /** * Is mobile device * @return bool */ public function isMobile() { return $this->request->isMobile(); } /** * Is tablet device * @return bool */ public function isTablet() { return $this->request->isTablet(); } /** * Is phone device * @return bool */ public function isPhone() { return $this->request->isPhone(); } }