Skip to content
Merged
40 changes: 40 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,45 @@ All notable changes to this project will be documented in this file.

This projects adheres to [Semantic Versioning](https://semver.org/) and [Keep a CHANGELOG](https://keepachangelog.com/).

## [13.0.0]

### Added

- Added a production service-classes cache (`{Namespace}ServiceClasses.json`) alongside the compiled PHP-DI container so the autowire scan only runs on a cold cache.
- Added a development service-classes cache (`{Namespace}DevServiceClasses.json`) keyed by the namespace mtime, disabled under WP-CLI so scaffolding always sees freshly added classes.
- Added stampede protection to `CacheTrait::populateCacheData()` via an advisory `flock()` lock with a double-checked load on contention.
- Added support in `Autowiring` for primitive constructor parameters that declare a default value — DI now registers classes that mix object and defaulted scalar dependencies instead of throwing.
- Added a shared `ReflectionClass` cache reused across the autowire build.
- Added a `PROPS_PASSTHROUGH_KEYS` constant in `AttributesTrait` for the special keys propagated through `props()`.

### Changed

- Promoted `Autowiring` constructor parameters to `readonly` properties and removed the now-redundant `AbstractMain::__construct()` (inherited from `Autowiring`).
- Rewrote `Autowiring::buildServiceClasses()` to resolve transitive dependencies through a work-queue instead of mutating the array under a `foreach` reference.
- Hoisted `Helpers::getSettings()` and wrapper attribute lookups out of the per-block loop in `AbstractBlocks::registerBlocks()` and threaded a shared `$context` into block registration.
- Memoized camel↔kebab conversions in `AbstractBlocks::prepareComponentAttribute()`/`prepareComponentAttributes()` and replaced per-iteration `array_merge` chains with a single varargs merge.
- Memoized `StoreBlocksTrait::getSettings()`, `TailwindTrait::getTwBreakpoints()`, `RenderTrait` default path name, and per-file existence checks for the request lifetime.
- Tightened parameter/return types in `TailwindTrait` (`unifyClasses`, `processOption`, `getTwBreakpoints`) and `AttributesTrait::checkAttr()` (now `: mixed`).
- `CssVariablesTrait::outputCssVariablesGlobalClean()` now builds the inner CSS with a single `implode()` and escapes the `id` / selector attributes via `esc_attr()` in `outputCssVariablesGlobal`/`outputCssVariablesInline`.
- `PathsTrait::joinPaths()` replaces `pathinfo()` extension detection with an inline `strrpos` check; `getProjectPaths()` uses spread-operator path composition.
- `GeneralTrait::isValidXml()` simplified to a single tight check; `flattenArray()` now preserves all non-`null` scalars; `recursiveArrayFind()` annotated as `array<int, mixed>`.
- `SelectorsTrait` serializers use `implode()` instead of trailing-space concatenation and route through `Helpers::clsx()` directly.
- `ComponentException::throwNotStringOrArray()` inverted to use `is_object()` instead of `gettype() !== 'object'`.
- Bumped dev dependencies: `dealerdirect/phpcodesniffer-composer-installer` → `1.2.1`, `infinum/eightshift-coding-standards` → `^3.1.0`.

### Removed

- Removed `RenderTrait::initializeRenderCaches()` and the `$renderHandlers` lookup table — dispatch is now inlined.
- Removed `AbstractMain::__construct()` (the promoted `Autowiring` constructor is used instead).

### Fixed

- Fixed `Autowiring::buildClasses()` filename regex so it requires the `.php` extension to terminate the filename (previously `Foo.phpinfo` would have matched).
- Fixed `Autowiring`/`AbstractMain` directory walks to skip `.` and `..` via `RecursiveDirectoryIterator::SKIP_DOTS`.
- Fixed `CssVariablesTrait` to call `Helpers::getSettingsGlobalVariablesBreakpoints()` instead of `self::` so the trait works when consumed by classes that don't import that helper directly.
- Fixed double `wp_json_encode()` of the manifest payload on the cache write path in `CacheTrait`.
- Fixed `flattenArray()` dropping legitimate falsy scalars (`0`, `false`, `''`).

## [12.3.4]

### Changed
Expand Down Expand Up @@ -1136,6 +1175,7 @@ Init setup
- Gutenberg Blocks Registration.
- Assets Manifest data.

[13.0.0]: https://github.com/infinum/eightshift-libs/compare/12.3.4...13.0.0
[12.3.4]: https://github.com/infinum/eightshift-libs/compare/12.3.3...12.3.4
[12.3.3]: https://github.com/infinum/eightshift-libs/compare/12.3.2...12.3.3
[12.3.2]: https://github.com/infinum/eightshift-libs/compare/12.3.1...12.3.2
Expand Down
4 changes: 2 additions & 2 deletions composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -32,8 +32,8 @@
"php-di/php-di": "^7.1.1"
},
"require-dev": {
"dealerdirect/phpcodesniffer-composer-installer": "1.2.0",
"infinum/eightshift-coding-standards": "^3.0.1",
"dealerdirect/phpcodesniffer-composer-installer": "1.2.1",
"infinum/eightshift-coding-standards": "^3.1.0",
"php-parallel-lint/php-parallel-lint": "^v1.4.0",
"php-stubs/wordpress-stubs": "6.9.1",
"szepeviktor/phpstan-wordpress": "2.0.3",
Expand Down
109 changes: 63 additions & 46 deletions src/Blocks/AbstractBlocks.php
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,20 @@
*/
abstract class AbstractBlocks implements ServiceInterface, RenderableBlockInterface
{
/**
* Memoized camel-to-kebab conversions for component names.
*
* @var array<string, string>
*/
private static array $camelToKebabMemo = [];

/**
* Memoized kebab-to-camel conversions for parent prefixes.
*
* @var array<string, string>
*/
private static array $kebabToCamelMemo = [];

/**
* Create custom project color palette.
* These colors are fetched from the main settings manifest.json.
Expand Down Expand Up @@ -65,16 +79,17 @@ public function getAllAllowedBlocksList($allowedBlockTypes, WP_Block_Editor_Cont
return $allowedBlockTypes;
}

$blocks = Helpers::getBlocks();
static $projectBlockNames = null;

if ($projectBlockNames === null) {
$blocks = Helpers::getBlocks();
$projectBlockNames = $blocks
? \array_values(\array_map(static fn($block) => $block['blockFullName'], $blocks))
: [];
}

if ($blocks) {
$allowedBlockTypes = \array_values(\array_merge(
\array_map(
fn($block) => $block['blockFullName'],
$blocks
),
$allowedBlockTypes,
));
if ($projectBlockNames) {
$allowedBlockTypes = \array_values(\array_merge($projectBlockNames, $allowedBlockTypes));
}

// Allow reusable block.
Expand Down Expand Up @@ -110,8 +125,15 @@ public function getAllBlocksList($allowedBlockTypes, WP_Block_Editor_Context $bl
*/
public function registerBlocks(): void
{
$settings = Helpers::getSettings();
$context = [
'blockClassPrefix' => $settings['blockClassPrefix'] ?? 'block',
'settingsAttributes' => $settings['attributes'] ?? [],
'wrapperAttributes' => Helpers::getConfigUseWrapper() ? (Helpers::getWrapper()['attributes'] ?? []) : [],
];

foreach (Helpers::getBlocks() as $block) {
$this->registerBlock($block);
$this->registerBlock($block, $context);
}
}

Expand Down Expand Up @@ -230,10 +252,11 @@ public function outputCssVariablesGlobal(): void
* It uses native register_block_type() function from WP.
*
* @param array<string, mixed> $blockDetails Full Block Manifest details.
* @param array<string, mixed> $context Shared registration context (blockClassPrefix, settingsAttributes, wrapperAttributes).
*
* @return void
*/
private function registerBlock(array $blockDetails): void
private function registerBlock(array $blockDetails, array $context): void
{
if (($blockDetails['active'] ?? true) === false) {
return;
Expand All @@ -243,7 +266,7 @@ private function registerBlock(array $blockDetails): void
$blockDetails['blockFullName'],
[
'render_callback' => [$this, 'render'],
'attributes' => $this->getAttributes($blockDetails),
'attributes' => $this->getAttributes($blockDetails, $context),
]
);
}
Expand All @@ -257,19 +280,14 @@ private function registerBlock(array $blockDetails): void
* Also it is doing recursive loop for all children components and their attributes.
*
* @param array<string, mixed> $blockDetails Block Manifest details.
* @param array<string, mixed> $context Shared registration context (blockClassPrefix, settingsAttributes, wrapperAttributes).
*
* @return array<string, mixed>
*/
private function getAttributes(array $blockDetails): array
private function getAttributes(array $blockDetails, array $context): array
{
$blockName = $blockDetails['blockName'];
$blockClassPrefix = Helpers::getSettings()['blockClassPrefix'] ?? 'block';

$wrapperAttributes = [];

if (Helpers::getConfigUseWrapper()) {
$wrapperAttributes = Helpers::getWrapper()['attributes'] ?? [];
}
$blockClassPrefix = $context['blockClassPrefix'];

return \array_merge(
[
Expand Down Expand Up @@ -302,8 +320,8 @@ private function getAttributes(array $blockDetails): array
'default' => false,
],
],
Helpers::getSettings()['attributes'] ?? [],
$wrapperAttributes,
$context['settingsAttributes'],
$context['wrapperAttributes'],
$this->prepareComponentAttributes($blockDetails)
);
}
Expand Down Expand Up @@ -332,32 +350,36 @@ private function prepareComponentAttribute(array $manifest, string $newName, str
}

// Make sure the case is always correct for parent.
$newParent = Helpers::kebabToCamelCase($parent);
$newParent = self::$kebabToCamelMemo[$parent] ??= Helpers::kebabToCamelCase($parent);

// Precompute the prefix that gets stripped when currentAttributes is true.
$currentPrefix = $currentAttributes
? \lcfirst(self::$kebabToCamelMemo[$realName] ??= Helpers::kebabToCamelCase($realName))
: '';

// Iterate each attribute and attach parent prefixes.
$componentAttributeKeys = \array_keys($componentAttributes);
foreach ($componentAttributeKeys as $componentAttribute) {
$attribute = $componentAttribute;
foreach ($componentAttributes as $componentAttribute => $attributeValue) {
$attribute = (string) $componentAttribute;

// If there is an attribute name switch, use the new one.
if ($newName !== $realName) {
$attribute = \str_replace($realName, $newName, (string) $componentAttribute);
$attribute = \str_replace($realName, $newName, $attribute);
}

// Check if current attribute is used strip component prefix from attribute and replace it with parent prefix.
if ($currentAttributes) {
$attribute = \str_replace(\lcfirst(Helpers::kebabToCamelCase($realName)), '', (string) $componentAttribute);
$attribute = \str_replace($currentPrefix, '', (string) $componentAttribute);
}

// Determine if parent is empty and if parent name is the same as component/block name and skip wrapper attributes.
if (\substr((string)$attribute, 0, \strlen('wrapper')) === 'wrapper') {
if (\str_starts_with($attribute, 'wrapper')) {
$attributeName = $attribute;
} else {
$attributeName = $newParent . \ucfirst((string)$attribute);
$attributeName = $newParent . \ucfirst($attribute);
}

// Output new attribute names.
$output[$attributeName] = $componentAttributes[$componentAttribute];
$output[$attributeName] = $attributeValue;
}

return $output;
Expand All @@ -376,8 +398,6 @@ private function prepareComponentAttribute(array $manifest, string $newName, str
*/
private function prepareComponentAttributes(array $manifest, string $parent = ''): array
{
$output = [];

// Determine if this is component or block and provide the name, not used for anything important but only to output the error msg.
$name = $manifest['blockName'] ?? '';

Expand All @@ -389,10 +409,13 @@ private function prepareComponentAttributes(array $manifest, string $parent = ''

$newParent = ($parent === '') ? $name : $parent;

$collected = [];

// Iterate over components key in manifest recursively and check component names.
foreach ($components as $newComponentName => $realComponentName) {
// Filter components real name.
$component = Helpers::getComponent(Helpers::camelToKebabCase($realComponentName));
$realKebab = self::$camelToKebabMemo[$realComponentName] ??= Helpers::camelToKebabCase($realComponentName);
$component = Helpers::getComponent($realKebab);

// Bailout if component doesn't exist.
if (!$component) {
Expand All @@ -401,22 +424,16 @@ private function prepareComponentAttributes(array $manifest, string $parent = ''

// If component has more components do recursive loop.
if (isset($component['components'])) {
$outputAttributes = $this->prepareComponentAttributes($component, $newParent . \ucfirst(Helpers::camelToKebabCase($newComponentName)));
$newKebab = self::$camelToKebabMemo[$newComponentName] ??= Helpers::camelToKebabCase($newComponentName);
$collected[] = $this->prepareComponentAttributes($component, $newParent . \ucfirst($newKebab));
} else {
// Output the component attributes if there is no nesting left, and append the parent prefixes.
$outputAttributes = $this->prepareComponentAttribute($component, $newComponentName, $realComponentName, $newParent);
$collected[] = $this->prepareComponentAttribute($component, $newComponentName, $realComponentName, $newParent);
}

// Populate the output recursively.
$output = \array_merge(
$output,
$outputAttributes
);
}

return \array_merge(
$output,
$this->prepareComponentAttribute($manifest, '', $name, $newParent, true)
);
$collected[] = $this->prepareComponentAttribute($manifest, '', $name, $newParent, true);

return \array_merge(...$collected);
}
}
6 changes: 3 additions & 3 deletions src/Exception/ComponentException.php
Original file line number Diff line number Diff line change
Expand Up @@ -28,15 +28,15 @@ final class ComponentException extends InvalidArgumentException implements Gener
*/
public static function throwNotStringOrArray($variable): ComponentException
{
if (\gettype($variable) !== 'object') {
if (\is_object($variable)) {
$output = \esc_html__('Object couldn\'t be converted to string. Please provide only string or array.', 'eightshift-libs');
} else {
$output = \sprintf(
/* translators: %1$s is replaced with the name of the variable, and %2$s with its type. */
\esc_html__('%1$s variable is not a string or array but rather %2$s', 'eightshift-libs'),
$variable,
\gettype($variable)
);
} else {
$output = \esc_html__('Object couldn\'t be converted to string. Please provide only string or array.', 'eightshift-libs');
}

return new ComponentException($output);
Expand Down
Loading
Loading