Skip to content
Open
97 changes: 97 additions & 0 deletions libraries/src/Form/CachingFormFactory.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
<?php

/**
* @package Joomla.CMS
* @subpackage Form
*
* @copyright (C) 2005 - 2024 Open Source Matters, Inc. <https://www.joomla.org>
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

still not correct - please do what I asked. Its is really not hard. Just look at the copyright headers in the same folder. Or maybe you arent actually doing anything other than asking AI to do it for you

* @license GNU General Public License version 2 or later; see LICENSE.txt
*/

declare(strict_types=1);

namespace Joomla\CMS\Form;

/**
* Caching decorator for the Form factory.
*
* - No interface/signature changes (BC-safe).
* - Reuses the same Form instance for a given (name, options) key within a request.
* - Allows bypassing cache via ['fresh' => true] in $options.
*
* Register via DI to override the default binding of FormFactoryInterface:
* $container->share(FormFactoryInterface::class, function (\Joomla\DI\Container $c) {
* $inner = $c->get('core.form.factory'); // whatever concrete is bound as
* return new CachingFormFactory($inner);
* });
*/
final class CachingFormFactory implements FormFactoryInterface
{
public function __construct(private FormFactoryInterface $inner)
{
}

/** @var array<string, Form> */
private array $cache = [];
/**
* {@inheritdoc}
*/
public function createForm(string $name, array $options = []): Form
{
// Allow callers to opt out of caching explicitly.
if (!empty($options['fresh'])) {
// Do not store in cache when 'fresh' is requested.
$opts = $options;
unset($opts['fresh']);
return $this->inner->createForm($name, $opts);
}

$key = $this->makeKey($name, $options);
return $this->cache[$key] ??= $this->inner->createForm($name, $this->normalizedOptions($options));
}

/**
* Removes a cached Form for the given name/options combination.
* Useful when a caller knows the underlying XML or dynamic fields changed mid-request.
*/
public function invalidate(string $name, array $options = []): void
{
$key = $this->makeKey($name, $options);
unset($this->cache[$key]);
}

/**
* Clears all cached Form instances (per-request scope).
*/
public function invalidateAll(): void
{
$this->cache = [];
}

/**
* Build a stable cache key from name + options.
* Excludes volatile/nonce-like options that shouldn't affect identity.
*/
private function makeKey(string $name, array $options): string
{
$opts = $this->normalizedOptions($options);
// Remove flags that should not influence identity:
unset(
$opts['fresh'], // our local bypass flag
$opts['debug'], // debugging shouldn't split cache entries
$opts['timestamp'] // any time-based hint
);
// Sort for deterministic encoding.
ksort($opts);
return $name . '|' . md5(json_encode($opts, JSON_THROW_ON_ERROR));
}

/**
* Normalize options to ensure deterministic keys and pass-through.
*/
private function normalizedOptions(array $options): array
{
// Ensure consistent types/casing if needed. Adjust as your concrete factory expects.
return $options;
}
}
87 changes: 87 additions & 0 deletions libraries/src/Service/CachingFactoriesProvider.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
<?php

/**
* @package Joomla.CMS
* @subpackage Service
*
* @copyright (C) 2005 - 2024 Open Source Matters, Inc. <https://www.joomla.org>
* @license GNU General Public License version 2 or later; see LICENSE.txt
*/

declare(strict_types=1);

namespace Joomla\CMS\Service;

use Joomla\CMS\Form\CachingFormFactory;
use Joomla\CMS\Form\FormFactoryInterface;
use Joomla\CMS\User\CachingUserFactory;
use Joomla\CMS\User\UserFactoryInterface;
use Joomla\DI\Container;
use Joomla\DI\ServiceProviderInterface;

/**
* Registers caching decorator factories for Forms and Users.
*
* Default behavior (BC-safe):
* - Adds opt-in services 'caching.form.factory' and 'caching.user.factory'
* without changing existing bindings.
*
* Optional behavior:
* - If $replaceDefaults is true, replaces the default bindings of
* FormFactoryInterface and UserFactoryInterface with the caching decorators.
*
* Usage:
* // BC-safe, opt-in only:
* $container->registerServiceProvider(new CachingFactoriesProvider());
*
* // Replace defaults globally (still no interface changes):
* $container->registerServiceProvider(new CachingFactoriesProvider(true));
*/
final class CachingFactoriesProvider implements ServiceProviderInterface
{
public function __construct(private bool $replaceDefaults = false)
{
}

public function register(Container $container): void
{
// ---- Opt-in services (always provided) ------------------------------

// caching.form.factory: a CachingFormFactory that wraps the current default form factory
$container->share('caching.form.factory', function (Container $c) {

// Resolve whatever is currently bound for the interface
$inner = $c->get(FormFactoryInterface::class);
return new CachingFormFactory($inner);
});
// caching.user.factory: a CachingUserFactory that wraps the current default user factory
$container->share('caching.user.factory', function (Container $c) {

$inner = $c->get(UserFactoryInterface::class);
return new CachingUserFactory($inner);
});
// ---- Optional: replace defaults (no BC break; interfaces unchanged) --

if ($this->replaceDefaults) {
// Override the interface bindings with the caching decorators
$container->share(FormFactoryInterface::class, function (Container $c) {

// Wrap the original concrete (obtain via previous binding)
$inner = $c->get('core.form.factory') ?? $c->get('caching.form.factory');
// If 'core.form.factory' is not registered, fall back to opt-in service
if ($inner instanceof CachingFormFactory) {
return $inner;
}
return new CachingFormFactory($inner);
});
$container->share(UserFactoryInterface::class, function (Container $c) {

$inner = $c->get('core.user.factory') ?? $c->get('caching.user.factory');
if ($inner instanceof CachingUserFactory) {
return $inner;
}
return new CachingUserFactory($inner);
});
}
}
}
99 changes: 99 additions & 0 deletions libraries/src/User/CachingUserFactory.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
<?php

/**
* @package Joomla.CMS
* @subpackage User
*
* @copyright (C) 2005 - 2024 Open Source Matters, Inc. <https://www.joomla.org>
* @license GNU General Public License version 2 or later; see LICENSE.txt
*/
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is not correct. Please do what I said and look at the other files in the same folder!


declare(strict_types=1);

namespace Joomla\CMS\User;

/**
* Caching decorator for the User factory.
*
* - BC-safe: implements UserFactoryInterface without changing signatures.
* - Adds per-request identity maps for id and username.
*/
final class CachingUserFactory implements UserFactoryInterface
{
/** @var array<int, User> */
private array $byId = [];

/** @var array<string, User> */
private array $byUsername = [];

public function __construct(private UserFactoryInterface $inner)
{
}

/**
* {@inheritdoc}
*/
public function loadUserById(int $id): User
{
if (isset($this->byId[$id])) {
return $this->byId[$id];
}

$user = $this->inner->loadUserById($id);

// Keep maps in sync
$this->byId[$user->id] = $user;

if (isset($user->username) && \is_string($user->username) && $user->username !== '') {
$this->byUsername[$user->username] = $user;
}

return $user;
}

/**
* {@inheritdoc}
*/
public function loadUserByUsername(string $username): User
{
if (isset($this->byUsername[$username])) {
return $this->byUsername[$username];
}

$user = $this->inner->loadUserByUsername($username);

if (\is_int($user->id)) {
$this->byId[$user->id] = $user;
}

$this->byUsername[$username] = $user;

return $user;
}

/**
* Invalidate a single cached user by id, if needed.
*/
public function invalidateById(int $id): void
{
if (!isset($this->byId[$id])) {
return;
}

$user = $this->byId[$id];
unset($this->byId[$id]);

if (isset($user->username)) {
unset($this->byUsername[(string) $user->username]);
}
}

/**
* Clear all cached User instances (per-request scope).
*/
public function invalidateAll(): void
{
$this->byId = [];
$this->byUsername = [];
}
}
Binary file added php-cs-fixer.phar
Binary file not shown.