diff --git a/CHANGELOG.md b/CHANGELOG.md index 5e2e9fd13..034ba3db4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,11 +1,16 @@ # v1.6.0-rc.5 ## mm/dd/2019 +1. [](#new) + * Added `FlexObjectInterface::getDefaultValue()` and `FormInterface::getDefaultValue()` 1. [](#improved) * Added `Content-Type: application/json` body support for PSR-7 `ServerRequest` * Remove PHP time limit in `ZipArchive` + * DebugBar: Resolve twig templates in deprecated backtraces in order to help locating Twig issues 1. [](#bugfix) * Fixed `Undefined method closure::fields()` when getting avatar for user, thanks @Romarain [#2422](https://github.com/getgrav/grav/issues/2422) + * Grav 1.6: Added `Flex[Class]::getFlexType()` to all flex classes to have the interface for all Flex classes + * FlexObjects: Remove null values during save # v1.6.0-rc.4 ## 03/20/2019 diff --git a/system/src/Grav/Common/Data/Blueprint.php b/system/src/Grav/Common/Data/Blueprint.php index 24720c783..49b4aca06 100644 --- a/system/src/Grav/Common/Data/Blueprint.php +++ b/system/src/Grav/Common/Data/Blueprint.php @@ -25,6 +25,9 @@ class Blueprint extends BlueprintForm /** @var BlueprintSchema */ protected $blueprintSchema; + /** @var array */ + protected $defaults; + public function setScope($scope) { $this->scope = $scope; @@ -56,7 +59,11 @@ class Blueprint extends BlueprintForm { $this->initInternals(); - return $this->blueprintSchema->getDefaults(); + if (null === $this->defaults) { + $this->defaults = $this->blueprintSchema->getDefaults(); + } + + return $this->defaults; } /** @@ -174,6 +181,7 @@ class Blueprint extends BlueprintForm $this->blueprintSchema->embed('', $this->items); $this->blueprintSchema->init(); + $this->defaults = null; } } diff --git a/system/src/Grav/Common/Debugger.php b/system/src/Grav/Common/Debugger.php index 86b3d888c..3afce3798 100644 --- a/system/src/Grav/Common/Debugger.php +++ b/system/src/Grav/Common/Debugger.php @@ -21,6 +21,9 @@ use DebugBar\DebugBar; use DebugBar\JavascriptRenderer; use DebugBar\StandardDebugBar; use Grav\Common\Config\Config; +use Grav\Common\Processors\ProcessorInterface; +use Twig\Template; +use Twig\TemplateWrapper; class Debugger { @@ -44,7 +47,7 @@ class Debugger /** @var array */ protected $timers = []; - /** @var string[] $deprecations */ + /** @var array $deprecations */ protected $deprecations = []; /** @var callable */ @@ -356,57 +359,183 @@ class Debugger return true; } - $backtrace = debug_backtrace(false); + // Figure out error scope from the error. + $scope = 'unknown'; + if (stripos($errstr, 'grav') !== false) { + $scope = 'grav'; + } elseif (strpos($errfile, '/twig/') !== false) { + $scope = 'twig'; + } elseif (stripos($errfile, '/yaml/') !== false) { + $scope = 'yaml'; + } elseif (strpos($errfile, '/vendor/') !== false) { + $scope = 'vendor'; + } + + // Clean up backtrace to make it more useful. + $backtrace = debug_backtrace(DEBUG_BACKTRACE_PROVIDE_OBJECT); // Skip current call. array_shift($backtrace); + // Find yaml file where the error happened. + if ($scope === 'yaml') { + foreach ($backtrace as $current) { + if (isset($current['args'])) { + foreach ($current['args'] as $arg) { + if ($arg instanceof \SplFileInfo) { + $arg = $arg->getPathname(); + } + if (\is_string($arg) && preg_match('/.+\.(yaml|md)$/i', $arg)) { + $errfile = $arg; + $errline = 0; + + break 2; + } + } + } + } + } + + // Filter arguments. + $cut = 0; + $previous = null; + foreach ($backtrace as $i => &$current) { + if (isset($current['args'])) { + $args = []; + foreach ($current['args'] as $arg) { + if (\is_string($arg)) { + $arg = "'" . $arg . "'"; + if (mb_strlen($arg) > 100) { + $arg = 'string'; + } + } elseif (\is_bool($arg)) { + $arg = $arg ? 'true' : 'false'; + } elseif (\is_scalar($arg)) { + $arg = $arg; + } elseif (\is_object($arg)) { + $arg = get_class($arg) . ' $object'; + } elseif (\is_array($arg)) { + $arg = '$array'; + } else { + $arg = '$object'; + } + + $args[] = $arg; + } + $current['args'] = $args; + } + + $object = $current['object'] ?? null; + unset($current['object']); + + $reflection = null; + if ($object instanceof TemplateWrapper) { + $reflection = new \ReflectionObject($object); + $property = $reflection->getProperty('template'); + $property->setAccessible(true); + $object = $property->getValue($object); + } + + if ($object instanceof Template) { + $file = $current['file'] ?? null; + + if (preg_match('`(Template.php|TemplateWrapper.php)$`', $file)) { + $current = null; + continue; + } + + $debugInfo = $object->getDebugInfo(); + + $line = 1; + if (!$reflection) { + foreach ($debugInfo as $codeLine => $templateLine) { + if ($codeLine <= $current['line']) { + $line = $templateLine; + break; + } + } + } + + $src = $object->getSourceContext(); + //$code = preg_split('/\r\n|\r|\n/', $src->getCode()); + //$current['twig']['twig'] = trim($code[$line - 1]); + $current['twig']['file'] = $src->getPath(); + $current['twig']['line'] = $line; + + $prevFile = $previous['file'] ?? null; + if ($prevFile && $file === $prevFile) { + $prevLine = $previous['line']; + + $line = 1; + foreach ($debugInfo as $codeLine => $templateLine) { + if ($codeLine <= $prevLine) { + $line = $templateLine; + break; + } + } + + //$previous['twig']['twig'] = trim($code[$line - 1]); + $previous['twig']['file'] = $src->getPath(); + $previous['twig']['line'] = $line; + } + + $cut = $i; + } elseif ($object instanceof ProcessorInterface) { + $cut = $cut ?: $i; + break; + } + + $previous = &$backtrace[$i]; + } + unset($current); + + if ($cut) { + $backtrace = array_slice($backtrace, 0, $cut + 1); + } + $backtrace = array_values(array_filter($backtrace)); + // Skip vendor libraries and the method where error was triggered. - while ($current = array_shift($backtrace)) { - if (isset($current['file']) && strpos($current['file'], 'vendor') !== false) { + foreach ($backtrace as $i => $current) { + if (!isset($current['file'])) { + continue; + } + if (strpos($current['file'], '/vendor/') !== false) { + $cut = $i + 1; continue; } if (isset($current['function']) && ($current['function'] === 'user_error' || $current['function'] === 'trigger_error')) { - $current = array_shift($backtrace); + $cut = $i + 1; + continue; } break; } - // Add back last call. - array_unshift($backtrace, $current); - - // Filter arguments. - foreach ($backtrace as &$current) { - if (isset($current['args'])) { - $args = []; - foreach ($current['args'] as $arg) { - if (\is_string($arg)) { - $args[] = "'" . $arg . "'"; - } elseif (\is_bool($arg)) { - $args[] = $arg ? 'true' : 'false'; - } elseif (\is_scalar($arg)) { - $args[] = $arg; - } elseif (\is_object($arg)) { - $args[] = get_class($arg) . ' $object'; - } elseif (\is_array($arg)) { - $args[] = '$array'; - } else { - $args[] = '$object'; - } - } - $current['args'] = $args; - } + if ($cut) { + $backtrace = array_slice($backtrace, $cut); } - unset($current); + $backtrace = array_values(array_filter($backtrace)); - $this->deprecations[] = [ + $current = reset($backtrace); + + // If the issue happened inside twig file, change the file and line to match that file. + $file = $current['twig']['file'] ?? ''; + if ($file) { + $errfile = $file; + $errline = $current['twig']['line'] ?? 0; + } + + $deprecation = [ + 'scope' => $scope, 'message' => $errstr, 'file' => $errfile, 'line' => $errline, 'trace' => $backtrace, + 'count' => 1 ]; + $this->deprecations[] = $deprecation; + // Do not pass forward. return true; } @@ -431,38 +560,37 @@ class Debugger protected function getDepracatedMessage($deprecated) { - $scope = 'unknown'; - if (stripos($deprecated['message'], 'grav') !== false) { - $scope = 'grav'; - } elseif (!isset($deprecated['file'])) { - $scope = 'unknown'; - } elseif (stripos($deprecated['file'], 'twig') !== false) { - $scope = 'twig'; - } elseif (stripos($deprecated['file'], 'yaml') !== false) { - $scope = 'yaml'; - } elseif (stripos($deprecated['file'], 'vendor') !== false) { - $scope = 'vendor'; - } + $scope = $deprecated['scope']; $trace = []; - foreach ($deprecated['trace'] as $current) { - $class = $current['class'] ?? ''; - $type = $current['type'] ?? ''; - $function = $this->getFunction($current); - if (isset($current['file'])) { - $current['file'] = str_replace(GRAV_ROOT . '/', '', $current['file']); + if (isset($deprecated['trace'])) { + foreach ($deprecated['trace'] as $current) { + $class = $current['class'] ?? ''; + $type = $current['type'] ?? ''; + $function = $this->getFunction($current); + if (isset($current['file'])) { + $current['file'] = str_replace(GRAV_ROOT . '/', '', $current['file']); + } + + unset($current['class'], $current['type'], $current['function'], $current['args']); + + if (isset($current['twig'])) { + $trace[] = $current['twig']; + } else { + $trace[] = ['call' => $class . $type . $function] + $current; + } } - - unset($current['class'], $current['type'], $current['function'], $current['args']); - - $trace[] = ['call' => $class . $type . $function] + $current; } + $array = [ + 'message' => $deprecated['message'], + 'file' => $deprecated['file'], + 'line' => $deprecated['line'], + 'trace' => $trace + ]; + return [ - [ - 'message' => $deprecated['message'], - 'trace' => $trace - ], + array_filter($array), $scope ]; } @@ -473,6 +601,6 @@ class Debugger return ''; } - return $trace['function'] . '(' . implode(', ', $trace['args']) . ')'; + return $trace['function'] . '(' . implode(', ', $trace['args'] ?? []) . ')'; } } diff --git a/system/src/Grav/Common/User/FlexUser/User.php b/system/src/Grav/Common/User/FlexUser/User.php index f97adfdb9..49084eb94 100644 --- a/system/src/Grav/Common/User/FlexUser/User.php +++ b/system/src/Grav/Common/User/FlexUser/User.php @@ -161,9 +161,9 @@ class User extends FlexObject implements UserInterface, MediaManipulationInterfa * @param string|null $separator * @return mixed */ - public function value($name, $default = null, $separator = null) + public function getFormValue(string $name, $default = null, string $separator = null) { - $value = parent::value($name, null, $separator); + $value = parent::getFormValue($name, null, $separator); if ($name === 'avatar') { return $this->parseFileProperty($value); diff --git a/system/src/Grav/Framework/Flex/Flex.php b/system/src/Grav/Framework/Flex/Flex.php index de5ddcd26..8f47c46bf 100644 --- a/system/src/Grav/Framework/Flex/Flex.php +++ b/system/src/Grav/Framework/Flex/Flex.php @@ -63,7 +63,7 @@ class Flex implements \Countable */ public function addDirectory(FlexDirectory $directory) { - $this->types[$directory->getType()] = $directory; + $this->types[$directory->getFlexType()] = $directory; return $this; } diff --git a/system/src/Grav/Framework/Flex/FlexCollection.php b/system/src/Grav/Framework/Flex/FlexCollection.php index a71ff604e..beacea8d8 100644 --- a/system/src/Grav/Framework/Flex/FlexCollection.php +++ b/system/src/Grav/Framework/Flex/FlexCollection.php @@ -87,7 +87,7 @@ class FlexCollection extends ObjectCollection implements FlexCollectionInterface parent::__construct($entries); if ($directory) { - $this->setFlexDirectory($directory)->setKey($directory->getType()); + $this->setFlexDirectory($directory)->setKey($directory->getFlexType()); } } @@ -129,7 +129,7 @@ class FlexCollection extends ObjectCollection implements FlexCollectionInterface */ public function getFlexType(): string { - return $this->_flexDirectory->getType(); + return $this->_flexDirectory->getFlexType(); } /** @@ -245,7 +245,7 @@ class FlexCollection extends ObjectCollection implements FlexCollectionInterface * {@inheritdoc} * @see FlexCollectionInterface::render() */ - public function render($layout = null, array $context = []) + public function render(string $layout = null, array $context = []) { if (null === $layout) { $layout = 'default'; diff --git a/system/src/Grav/Framework/Flex/FlexDirectory.php b/system/src/Grav/Framework/Flex/FlexDirectory.php index d91b5564b..7b50a7eed 100644 --- a/system/src/Grav/Framework/Flex/FlexDirectory.php +++ b/system/src/Grav/Framework/Flex/FlexDirectory.php @@ -91,8 +91,19 @@ class FlexDirectory implements FlexAuthorizeInterface /** * @return string + * @deprecated 1.6 Use ->getFlexType() method instead. */ public function getType(): string + { + user_error(__CLASS__ . '::' . __FUNCTION__ . '() is deprecated since Grav 1.6, use ->getFlexType() method instead', E_USER_DEPRECATED); + + return $this->type; + } + + /** + * @return string + */ + public function getFlexType(): string { return $this->type; } @@ -102,7 +113,7 @@ class FlexDirectory implements FlexAuthorizeInterface */ public function getTitle(): string { - return $this->getBlueprintInternal()->get('title', ucfirst($this->getType())); + return $this->getBlueprintInternal()->get('title', ucfirst($this->getFlexType())); } /** @@ -312,13 +323,13 @@ class FlexDirectory implements FlexAuthorizeInterface if (Utils::isAdminPlugin()) { $key = substr($key, 0, -1); } - $cache = new DoctrineCache($gravCache->getCacheDriver(), 'flex-objects-' . $this->getType() . $key, $timeout); + $cache = new DoctrineCache($gravCache->getCacheDriver(), 'flex-objects-' . $this->getFlexType() . $key, $timeout); } catch (\Exception $e) { /** @var Debugger $debugger */ $debugger = Grav::instance()['debugger']; $debugger->addException($e); - $cache = new MemoryCache('flex-objects-' . $this->getType()); + $cache = new MemoryCache('flex-objects-' . $this->getFlexType()); } // Disable cache key validation. diff --git a/system/src/Grav/Framework/Flex/FlexForm.php b/system/src/Grav/Framework/Flex/FlexForm.php index 3e4d8265b..2629448ef 100644 --- a/system/src/Grav/Framework/Flex/FlexForm.php +++ b/system/src/Grav/Framework/Flex/FlexForm.php @@ -45,7 +45,7 @@ class FlexForm implements FlexFormInterface { $this->name = $name; $this->form = $form; - $uniqueId = $object->exists() ? $object->getStorageKey() : "{$object->getType()}:new"; + $uniqueId = $object->exists() ? $object->getStorageKey() : "{$object->getFlexType()}:new"; $this->setObject($object); $this->setId($this->getName()); $this->setUniqueId(md5($uniqueId)); @@ -73,7 +73,7 @@ class FlexForm implements FlexFormInterface $object = $this->getObject(); $name = $this->name ?: 'object'; - return "flex-{$object->getType()}-{$name}"; + return "flex-{$object->getFlexType()}-{$name}"; } /** @@ -101,6 +101,26 @@ class FlexForm implements FlexFormInterface return $value ?? $this->getObject()->value($name); } + public function getDefaultValue(string $name) + { + return $this->object->getDefaultValue($name); + } + + /** + * @return array + */ + public function getDefaultValues(): array + { + return $this->object->getDefaultValues(); + } + /** + * @return string + */ + public function getFlexType(): string + { + return $this->object->getFlexType(); + } + /** * @return FlexObjectInterface */ diff --git a/system/src/Grav/Framework/Flex/FlexIndex.php b/system/src/Grav/Framework/Flex/FlexIndex.php index 77a2d85ac..b3b133edc 100644 --- a/system/src/Grav/Framework/Flex/FlexIndex.php +++ b/system/src/Grav/Framework/Flex/FlexIndex.php @@ -104,7 +104,7 @@ class FlexIndex extends ObjectIndex implements FlexCollectionInterface, FlexInde */ public function getFlexType(): string { - return $this->_flexDirectory->getType(); + return $this->_flexDirectory->getFlexType(); } /** @@ -171,7 +171,7 @@ class FlexIndex extends ObjectIndex implements FlexCollectionInterface, FlexInde { // Get storage keys for the objects. $keys = []; - $type = $this->_flexDirectory->getType() . '.obj:'; + $type = $this->_flexDirectory->getFlexType() . '.obj:'; foreach ($this->getEntries() as $key => $value) { $keys[$key] = $value['flex_key'] ?? $type . $value['storage_key']; @@ -191,7 +191,7 @@ class FlexIndex extends ObjectIndex implements FlexCollectionInterface, FlexInde return $this; } - $type = $keyField === 'flex_key' ? $this->_flexDirectory->getType() . '.obj:' : ''; + $type = $keyField === 'flex_key' ? $this->_flexDirectory->getFlexType() . '.obj:' : ''; $entries = []; foreach ($this->getEntries() as $key => $value) { if (!isset($value['key'])) { @@ -221,7 +221,7 @@ class FlexIndex extends ObjectIndex implements FlexCollectionInterface, FlexInde * {@inheritdoc} * @see FlexCollectionInterface::render() */ - public function render($layout = null, array $context = []) + public function render(string $layout = null, array $context = []) { return $this->__call('render', [$layout, $context]); } diff --git a/system/src/Grav/Framework/Flex/FlexObject.php b/system/src/Grav/Framework/Flex/FlexObject.php index 4c8882187..ef8c4fd15 100644 --- a/system/src/Grav/Framework/Flex/FlexObject.php +++ b/system/src/Grav/Framework/Flex/FlexObject.php @@ -115,7 +115,7 @@ class FlexObject implements FlexObjectInterface, FlexAuthorizeInterface */ public function getFlexType(): string { - return $this->_flexDirectory->getType(); + return $this->_flexDirectory->getFlexType(); } /** @@ -186,7 +186,7 @@ class FlexObject implements FlexObjectInterface, FlexAuthorizeInterface */ public function getFlexKey(): string { - return $this->_storage['flex_key'] ?? $this->_flexDirectory->getType() . '.obj:' . $this->getStorageKey(); + return $this->_storage['flex_key'] ?? $this->_flexDirectory->getFlexType() . '.obj:' . $this->getStorageKey(); } /** @@ -357,7 +357,7 @@ class FlexObject implements FlexObjectInterface, FlexAuthorizeInterface * {@inheritdoc} * @see FlexObjectInterface::render() */ - public function render($layout = null, array $context = []) + public function render(string $layout = null, array $context = []) { if (null === $layout) { $layout = 'default'; @@ -616,9 +616,50 @@ class FlexObject implements FlexObjectInterface, FlexAuthorizeInterface /** * {@inheritdoc} - * @see FlexObjectInterface::value() + * @see FlexObjectInterface::getDefaultValue() */ - public function value($name, $default = null, $separator = null) + public function getDefaultValue(string $name, string $separator = null) + { + $separator = $separator ?: '.'; + $path = explode($separator, $name) ?: []; + $offset = array_shift($path) ?? ''; + + $current = $this->getDefaultValues(); + + if (!isset($current[$offset])) { + return null; + } + + $current = $current[$offset]; + + while ($path) { + $offset = array_shift($path); + + if ((\is_array($current) || $current instanceof \ArrayAccess) && isset($current[$offset])) { + $current = $current[$offset]; + } elseif (\is_object($current) && isset($current->{$offset})) { + $current = $current->{$offset}; + } else { + return null; + } + }; + + return $current; + } + + /** + * @return array + */ + public function getDefaultValues(): array + { + return $this->getBlueprint()->getDefaults(); + } + + /** + * {@inheritdoc} + * @see FlexObjectInterface::getFormValue() + */ + public function getFormValue(string $name, $default = null, string $separator = null) { if ($name === 'storage_key') { return $this->getStorageKey(); @@ -630,6 +671,19 @@ class FlexObject implements FlexObjectInterface, FlexAuthorizeInterface return $this->getNestedProperty($name, $default, $separator); } + /** + * @param string $name + * @param mixed|null $default + * @param string|null $separator + * @return mixed + * + * @deprecated 1.6 Use ->getFormValue() method instead. + */ + public function value($name, $default = null, $separator = null) + { + return $this->getFormValue($name, $default, $separator); + } + /** * Returns a string representation of this object. * diff --git a/system/src/Grav/Framework/Flex/Interfaces/FlexCommonInterface.php b/system/src/Grav/Framework/Flex/Interfaces/FlexCommonInterface.php index 89f12100f..482aef226 100644 --- a/system/src/Grav/Framework/Flex/Interfaces/FlexCommonInterface.php +++ b/system/src/Grav/Framework/Flex/Interfaces/FlexCommonInterface.php @@ -11,11 +11,8 @@ declare(strict_types=1); namespace Grav\Framework\Flex\Interfaces; -use Grav\Framework\ContentBlock\ContentBlockInterface; -use Grav\Framework\ContentBlock\HtmlBlock; use Grav\Framework\Flex\FlexDirectory; -use Twig\Error\LoaderError; -use Twig\Error\SyntaxError; +use Grav\Framework\Interfaces\RenderInterface; /** * Defines common interface shared with both Flex Objects and Collections. @@ -23,7 +20,7 @@ use Twig\Error\SyntaxError; * @used-by \Grav\Framework\Flex\FlexObject * @since 1.6 */ -interface FlexCommonInterface +interface FlexCommonInterface extends RenderInterface { /** * Get Flex Type of the object / collection. @@ -64,22 +61,4 @@ interface FlexCommonInterface * @return string Returns cache checksum. */ public function getCacheChecksum(): string; - - /** - * Renders the object / collection. - * - * @example {% render object layout 'edit' with { limited: true } %} - * @example {% render collection layout 'list' %} - * - * @param string $layout Layout name. - * @param array $context Context given to the renderer. - * - * @return ContentBlockInterface|HtmlBlock Returns `HtmlBlock` containing the rendered output. - * @throws \Exception - * @throws \Throwable - * @throws LoaderError - * @throws SyntaxError - * @api - */ - public function render($layout = null, array $context = []); } diff --git a/system/src/Grav/Framework/Flex/Interfaces/FlexObjectInterface.php b/system/src/Grav/Framework/Flex/Interfaces/FlexObjectInterface.php index d0f11f6e9..a10ee8447 100644 --- a/system/src/Grav/Framework/Flex/Interfaces/FlexObjectInterface.php +++ b/system/src/Grav/Framework/Flex/Interfaces/FlexObjectInterface.php @@ -129,7 +129,7 @@ interface FlexObjectInterface extends FlexCommonInterface, NestedObjectInterface * @throws \RuntimeException if object already exists. * @api */ - public function create($key = null); + public function create(string $key = null); /** * Save object into the storage. @@ -173,12 +173,36 @@ interface FlexObjectInterface extends FlexCommonInterface, NestedObjectInterface public function getForm(string $name = '', array $form = null); /** - * Form field compatibility. + * Returns default value suitable to be used in a form for the given property. * - * @param string $name Property name. - * @param mixed $default Default value. - * @param string $separator Optional nested property separator. - * @return mixed Returns value of the field. + * @see FlexObjectInterface::getForm() + * + * @param string $name Property name. + * @param string $separator Optional nested property separator. + * + * @return mixed|null Returns default value of the field, null if there is no default value. */ - public function value($name, $default = null, $separator = null); + public function getDefaultValue(string $name, string $separator = null); + + /** + * Returns default values suitable to be used in a form for the given property. + * + * @see FlexObjectInterface::getForm() + * + * @return array Returns default values. + */ + public function getDefaultValues(): array; + + /** + * Returns raw value suitable to be used in a form for the given property. + * + * @see FlexObjectInterface::getForm() + * + * @param string $name Property name. + * @param mixed $default Default value. + * @param string $separator Optional nested property separator. + * + * @return mixed Returns value of the field. + */ + public function getFormValue(string $name, $default = null, string $separator = null); } diff --git a/system/src/Grav/Framework/Form/Traits/FormTrait.php b/system/src/Grav/Framework/Form/Traits/FormTrait.php index 24d6a4b54..23f941093 100644 --- a/system/src/Grav/Framework/Form/Traits/FormTrait.php +++ b/system/src/Grav/Framework/Form/Traits/FormTrait.php @@ -113,6 +113,42 @@ trait FormTrait return $this->data[$name] ?? null; } + public function getDefaultValue(string $name) + { + $path = explode('.', $name) ?: []; + $offset = array_shift($path) ?? ''; + + $current = $this->getDefaultValues(); + + if (!isset($current[$offset])) { + return null; + } + + $current = $current[$offset]; + + while ($path) { + $offset = array_shift($path); + + if ((\is_array($current) || $current instanceof \ArrayAccess) && isset($current[$offset])) { + $current = $current[$offset]; + } elseif (\is_object($current) && isset($current->{$offset})) { + $current = $current->{$offset}; + } else { + return null; + } + }; + + return $current; + } + + /** + * @return array + */ + public function getDefaultValues(): array + { + return $this->getBlueprint()->getDefaults(); + } + /** * @param ServerRequestInterface $request * @return FormInterface|$this diff --git a/system/src/Grav/Framework/Interfaces/RenderInterface.php b/system/src/Grav/Framework/Interfaces/RenderInterface.php new file mode 100644 index 000000000..9185c1712 --- /dev/null +++ b/system/src/Grav/Framework/Interfaces/RenderInterface.php @@ -0,0 +1,38 @@ +render('custom', ['variable' => 'value']); + * @example {% render object layout 'custom' with { variable: 'value' } %} + * + * @param string|null $layout Layout to be used. + * @param array|null $context Extra context given to the renderer. + * + * @return ContentBlockInterface|HtmlBlock Returns `HtmlBlock` containing the rendered output. + * @api + */ + public function render(string $layout = null, array $context = []); +} diff --git a/system/src/Grav/Framework/Object/Property/ArrayPropertyTrait.php b/system/src/Grav/Framework/Object/Property/ArrayPropertyTrait.php index 2673f0c40..7092e15d5 100644 --- a/system/src/Grav/Framework/Object/Property/ArrayPropertyTrait.php +++ b/system/src/Grav/Framework/Object/Property/ArrayPropertyTrait.php @@ -95,7 +95,7 @@ trait ArrayPropertyTrait */ protected function getElements() { - return $this->_elements; + return array_filter($this->_elements, function ($val) { return $val !== null; }); } /** diff --git a/system/src/Grav/Framework/Object/Property/ObjectPropertyTrait.php b/system/src/Grav/Framework/Object/Property/ObjectPropertyTrait.php index efc0c7f05..5f8fdf0ac 100644 --- a/system/src/Grav/Framework/Object/Property/ObjectPropertyTrait.php +++ b/system/src/Grav/Framework/Object/Property/ObjectPropertyTrait.php @@ -183,7 +183,10 @@ trait ObjectPropertyTrait $elements = []; foreach ($properties as $offset => $value) { - $elements[$offset] = $this->offsetSerialize($offset, $value); + $serialized = $this->offsetSerialize($offset, $value); + if ($serialized !== null) { + $elements[$offset] = $this->offsetSerialize($offset, $value); + } } return $elements;