## RFC
### Summary
One common grief of the way elements work in MODX is th…at they have to be created via the manager, or inserted in the database through another bootstrap or element helper before they can be used. There are workarounds, but they're still workarounds and can cause friction.
In this RFC I'd like to propose a new way to create elements, focused primarily on elements that are part of installed packages, such as reusable snippets, building on the per-namespace `bootstrap.php` introduced in 3.0, [and described here](https://docs.modx.com/3.x/en/extending-modx/namespaces#bootstrapping-services). If you've not yet seen that, it basically means MODX requires `core/components/your-package/bootstrap.php` automatically, making it a useful entrypoint for code-only development.
The concept is to introduce some classes instantiated on the main modX class which are meant to register code-only elements. These then live side-by-side with elements in the database, but are _not_ actually mirrored; instead the code that runs a snippet or parses a chunk is adjusted to use these repositories.
I find code to be easiest to explain this with, so let's start with an example.
### Example
Signatures:
```` php
$modx->snippets->add(string $nameOfSnippet, callable $code);
$modx->plugins->add(string $nameOfSnippet, string $event, callable $code);
$modx->chunks->add(string $nameOfChunk, callable $code);
$modx->templates->add(string $nameOfTemplate, callable $code);
````
Note that for chunks and templates it expects a callable which will return the HTML to be parsed. The callable does get access to the scriptProperties though, so that could open up some sort of dynamic chunks/templates feature where they change depending on the provided properties.
Some simple inline examples:
```` php
$modx->snippets->add('get_todo', function ($modx, $scriptProperties) {
$todo = $modx->getObject(Todo::class, ['id' => $_GET['id']]);
return $todo ? $modx->getChunk('todo_single', $todo->toArray()) : '<p>Not found</p>');
});
$modx->chunks->add('todo_single', function($modx, $scriptProperties) {
return file_get_contents(__DIR__ . '/elements/chunks/todo_single.chunk.tpl');
});
````
By virtue of accepting any type of callable, more powerful elements (think complex snippets) could be placed in an invoke-able class and referenced by name, e.g.:
```` php
// bootstrap.php
$modx->snippets->add('commerce.cart', \modmore\Commerce\Snippets\Cart::class);
// Snippets/Cart.php
final class Cart
{
public function __invoke(modX $modx, array $scriptProperties)
{
$this->modx = $modx;
$this->scriptProperties = $scriptProperties;
// do complex stuff
$this->run()
return $this->output;
}
}
````
With auto-wiring and enabling more services in the dependency container, that could potentially allow constructor injection for more composable snippet classes:
```` php
final class Cart
{
public function __construct(Parser $parser, CacheManager $cacheManager)
{
// initialise variables
}
public function __invoke(modX $modx, array $scriptProperties)
{
// do complex stuff
return $this->parser->parse('template', $this->placeholders);
}
}
````
There could also be utility methods, for example static helpers for reading chunk/template files from a path:
````php
// bootstrap.php
// uses scandir() to find all chunks, and repeatedly calls
// $modx->chunks->add('filename without chunk.tpl', function() { file_get_contents($fullPath); })
// for every match it finds
RepositoryScanner::registerFromPath($modx->chunks, __DIR__ . 'elements/chunks/', '.chunk.tpl');
// same, but with different parameters it now fetches snippets to `include` dynamically:
RepositoryScanner::registerFromPath($modx->snippets, __DIR__ . 'elements/snippets/', '.snippet.php');
// only plugins would need a dedicated option as that (currently) requires an extra parameter to register the event
RepositoryScanner::registerPluginsFromPath($modx->plugins, __DIR__ . 'elements/snippets/', '.snippet.php');
$modx->snippets->add('commerce.cart', SnippetRepository::includeFile(__DIR__ . 'elements/snippets/cart.snippet.php'));
````
### Repository
The repository is a collection of elements registered with it. I'd suggest keeping it in memory only (as each bootstrap file will be called on each request regardless), however it certainly could also be serialised to cache if that were to boost performance.
Here's a mocked interface for a snippet repository (not final):
```` php
<?php
namespace MODX\Revolution\Elements;
use MODX\Revolution\modX;
interface SnippetRepository
{
public function __construct(modX $modx)
public function add(string $name, callable $callable): void
public function has(string $name): bool
public function get(string $name): Snippet class or array?*
public function run(string $name, array $properties): string
public function wrap(string $original, callable $callable): void
public function list(): array
}
````
Repositories for other types would be almost identical, and it may make sense to have a single base class/interface.
Each method interacts with a private array property holding the snippets (*either in an array or as a new type of object for better abstraction). Names are unique; `add`ing the same name twice results in the second one overriding the first entirely.
`wrap()` is an idea to allow some sort of middleware-like implementation where additional layers can be wrapped around a snippet. For example wrapping a standard snippet like `phpthumbof` could be used to filter the options passed to `phpthumbof`, and/or adjust the output. That's only a small part of this RFC, though, and would require more work to make a PoC.
### Interaction with the core
To allow this to be implemented in a BC-friendly way, all existing elements will continue to work as they do now. The core will however be expanded to check the appropriate repository when an element is requested.
For example, in `modX::runSnippet()` (and `modX::getChunk()` looks _very_ similar) the implementation would look like this:
````php
public function runSnippet($snippetName, array $params= []) {
$output= '';
if ($this->getParser()) {
$snippet= $this->parser->getElement(modSnippet::class, $snippetName);
if ($snippet instanceof modSnippet) {
$snippet->setCacheable(false);
$output= $snippet->process($params);
}
+ elseif ($this->snippets->has($snippetName)) {
+ $output = $this->snippets->run($snippetName, $params);
+ }
}
return $output;
}
````
An alternative implementation could also hook into `$this->parser->getElement` instead of the runSnippet/getChunk/invokeEvent methods. Templates would need to be implemented slightly differently regardless due to how they're part of the request/response handling.
### Interaction with the manager
The manager and standard elements, including current static elements, would still exist, be usable and editable. In fact: **elements created in the manager/database would take precedence over code-only elements**. This means a Todo package might provide a default `todo_single` chunk (like in the earlier example), however the site builder could decide to create a chunk **in the manager** with the same name to override its contents.
In terms of the elements tree, the `list()` methods should be used in the relevant processors to add code-only elements to the end of the list, with a different icon or different styling indicating they are "vendor-provided" or "read only". Clicking them may open a read-only view of the content (through the get() method) but they're not to be made editable.
They could perhaps automatically be grouped by namespace, however the interface example above does not account for that.
### Benefits
- After configuring the namespace, adding elements becomes _really_ easy and can be done without having to leave the IDE.
- No need to pack elements as objects in transport packages; for simple to moderate extras it would only require creating the namespace and putting the files in.
- Performance: this obviously needs testing and benchmarking of an implementation to be backed up by the science, but I suspect elements defined in this way may benefit from a moderate speed up over regular and current static elements. Putting a name and callable into an array is very quick, and executing said callable seems like it also ought to be faster than what currently goes on in modParser::getElement, even with file caching.
- BC friendly; it adds new functionality without breaking old. It could be gradually adopted and if we move quick enough, could still be in time for the 3.0 feature freeze planned with rc1.
- Promotes the use of dependency injection for snippets and plugins
### Downsides
- Likely to lead to a slight increase in memory usage because it keeps the elements in memory. However, as it only holds the callables which would likely be invokeable class names or `include` statements for anything sizable, this wont be enormous.
- Yet another way to do static elements (but a better one, I'd say)
- Compared to pasting html into a form field, this requires more knowledge of PHP to work with. However, as elements in the manager/database would not be deprecated, I don't see that as an obstacle. As mentioned at the start, I see this as primarily intended for those building re-usable packages where elements ought not to be editable regardless.
### Final note
While this post is long, I've not yet spent a ton of time on architecting this - only an hour or so.
There's likely to be flaws I've overlooked, or specifics of the interface/concepts that are better handled differently. I'd like to ask to please focus on the presented idea more than the nitty gritty details at this point.
If the general response is positive, I'll likely try to work on a functional proof of concept for the snippets repository so we can start worrying about implementation details with an actual complete example.