-
-
Notifications
You must be signed in to change notification settings - Fork 185
feat: Plugin extension autoloader #7035
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: develop-minor
Are you sure you want to change the base?
feat: Plugin extension autoloader #7035
Conversation
| $cache_folder = pathinfo($this->cache_file, PATHINFO_DIRNAME); | ||
| try { | ||
| Dir::remove($cache_folder); | ||
| } catch (\Throwable $th) {}; |
Check warning
Code scanning / PHPMD
Design Rules: EmptyCatchBlock Warning
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Fixed in commit Fix empty catch.
|
This looks very interesting. Give us a bit of time to review this, while we still struggle with the last v5 steps. |
|
Todo:
|
|
@plain-solutions-gmbh I finally had a chance to take a closer look again. Unfortunately, it's still quite far from being mergable. That's not to say it isn't functional, but the approach and code style differ greatly from other parts of Kirby. I'd be concerned about maintainability, etc. It would need to be much less smart and automatic. For example, it shouldn't be a list of tasks that works with reflections. Rather, it should be a very explicit list of dedicated methods that deal with autoloading the different extensions. It shouldn't manipulate variables passed as arguments via references, but rather return values from methods and set them in the main context. It wouldn't make sense to send you in circles working on this. That might end up being a frustrating experience, and we don't want that to be the result of such a well-intentioned code contribution. However, we also need to ensure the quality and style of the code we add to the core. We might eventually get to it ourselves, but it's not our highest priority at the moment. |
|
That’s sad to hear. For over three years, I’ve been working exclusively with the Kirby CMS codebase, and I thought I had largely adapted its style of writing code. I have to admit that I demanded a lot of flexibility from this feature and, as you mentioned, ended up building in a bit too much automation. As Bastian emphasized several times during the introduction of K5, a new feature often sparks ideas for further improvements — and narrowing those down is truly a challenge. If you could spare a little time, I’d really appreciate it if you could leave me a few line-comments on this pr. That way, I can see where I’ve strayed furthest from Kirby’s code style and work on improving the quality of my code. Best regard Roman |
distantnative
left a comment
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I left some in-code comments. I think you are right that the biggest "problem" currently is that the code tries to be too flexible, too magic, too smart. We have all been there ourselves (and will be again now and then), but we have learned that this usually backfires down the road. Refactoring those tasks/usertasks and magic walker methods into very concrete methods for each extension would be an important step.
| public function allowedExtensionsKeys(): array | ||
| { | ||
| return array_keys($this->extensions); | ||
| } | ||
|
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We don't need to base the logic on the available extensions. It might seem like a good idea to make it dynamic based on this, but some verbosity will go a long way here for understanding and maintaining this (even with a bit more effort). Look at classes like Kirby\Cms\Core, Kirby\Cms\AppPlugins - writing code for each extension manually helps, even if that means we have to change the code in multiple classes when we add/remove an extension.
|
Thank you for your detailed explanation. I took the liberty of redesigning the autoloader based on your input. This time, I removed everything that made it confusing and slow. This means that caching is no longer necessary. Now you can simply pass an extended autoloader class to the plugin and you're already extremely flexible. |
distantnative
left a comment
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think this was a good step in the right direction!
I would encourage you to even less think of flexibility. Simplicity might be the more important first goal. Once we have a simple, solid, working solution, we could still see where to make it more flexible.
One thought that came up while reviewing and I just want to share it, might not work in the end. But instead of defining a separate autoload option in the plugin registration call, couldn't we use the logic of e.g. for blueprints "if the blueprint extend is an array, use it as before. If it is a string, assume it's a directory and automatically autoload it"?
| if ($autoloader) { | ||
|
|
||
| //Allow to apply custom Autoloader | ||
| $autolader_class = is_bool($autoloader) ? Autoloader::class : $autoloader; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Question: What for would one use a custom autoloader class?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
For example: In one of my cases i used it to extend the $this->extend() with data from a Class (Which is loading in the autoloader). So i made a custom Autoloader class and modified the toArray() method.
An other case could be, that you need to load snippets from another folder than snippets.
| $classfolder = $root . '/classes/'; | ||
| if (Dir::exists($classfolder)) { | ||
| $this->_classes($classfolder); | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I would move this to a loadClasses() method.
| foreach (Dir::dirs($this->root) as $dir) { | ||
| if (method_exists($this, $dir)) { | ||
| $this->$dir($this->root . '/' . $dir . '/'); | ||
| } | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This should be less magic. We can simply list all potential extensions:
$this->loadBlueprints();
$this->loadFields();
....
src/Plugin/Autoloader.php
Outdated
| public function _dirWalker(string $root, Closure $fnc) | ||
| { | ||
|
|
||
| foreach (Dir::index($root, true) as $path) { | ||
|
|
||
| //Check if the path is active | ||
| if (Str::contains($path, '/_')) { | ||
| continue; | ||
| } | ||
|
|
||
| $file = $root . $path; | ||
| if (F::exists($file)) { | ||
| $dirname = F::dirname($path); | ||
| $dirname = $dirname === '.' ? '' : $dirname . '/'; | ||
| $fnc($dirname . F::name($path), $file); | ||
| } | ||
| } | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think a less flexible and more explicit approach would be better. Reading the code, the _dirWalker method comes across quite cryptic and hard to understand. I also don't understand why many extensions would need to run the index vs. just Dir::files(). That's why IMO it would be better to leave it to each dedicated load*() method to use the appropriate code and not this meta method.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
That was my initial plan. However, this makes the entire file very repetitive. I use index so that I can include all subfolders at once.
src/Cms/AppPlugins.php
Outdated
| $extends = $autolader_class::load( | ||
| name: $name, | ||
| root: $root, | ||
| data: $extends ?? [] |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Suggestion: Instead of passing $extends to the class and merging it in the class's ::toArray() method, the class could be responsible to only gather and return autoloader extensions. And then we rather merge the explicitly passed $extends and the result of Autoloader::toArray() here.
src/Plugin/Autoloader.php
Outdated
| public function blueprints(string $root) | ||
| { | ||
| $this->_dirWalker($root, function ($path, $file) { | ||
|
|
||
| $name = F::relativepath($file); | ||
| //Set YAML-File or they content | ||
| $this->extends['blueprints'][$path] = (F::extension($file) === 'yml') ? $file : $this->_read($file); | ||
| }); | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
As an example, I think this could be refactored more in this direction:
| public function blueprints(string $root) | |
| { | |
| $this->_dirWalker($root, function ($path, $file) { | |
| $name = F::relativepath($file); | |
| //Set YAML-File or they content | |
| $this->extends['blueprints'][$path] = (F::extension($file) === 'yml') ? $file : $this->_read($file); | |
| }); | |
| } | |
| public function loadBlueprints(): void | |
| { | |
| $root = $this->root . '/blueprints'; | |
| foreach (Dir::index($root, true) as $blueprint) { | |
| $file = $root . '/' . $blueprint; | |
| if (F::exists($file) === false) { | |
| continue; | |
| } | |
| $this->extends['blueprints'][$blueprint] = $$file; | |
| } | |
| } |
(Just writing this on GitHub so code is probably not fully right, but to communicate the general idea)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
A good point. Would that mean that an array containing all specific folders with methods? For example, with:
$folders = [
'snippets' => 'loadSnippets'
...
];
foreach (Dir::dirs($this->root) as $dir) {
if (array_key_exists($dir, $folders) === false) {
$method = $folders[$dir];
$this->$method($this->root . '/' . $dir . '/');
}
}
´´´
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I would not try to make it that smart/magic. I know it seems very verbose and repetitive, but from our experience "boring" code like that has often paid off down the road.
|
Alright. Looks nicer now. |
That thought occurred to me as well. Developers typically select the 'Blueprint' folder to store blueprints, etc. For me, the development of the autoloader is also about making it easier for developers to get started with plugin development. Writing definitions that are self-explanatory would be an unnecessary barrier. |
Description
I have been working on a adequate solution for an plugin autoloader.
Here is an example:
If true is passed to the autoloader, all tasks defined in the autoloader are executed.
Summary of changes
Reasoning
Plugins (if the developer use the autoloader) can be loaded from cache.
Extensions could be easly integrating into the plugin.
Additional context
I create it because of this rejected pull request
It would be nice if it's goes into V5. Cause Plugin already takes a new parameter
licenseChangelog
./site/cache/autoloder/{vendor}/{plugin_name}\{timestamp}.php. (Can be deactivated)./configin charge.Docs
I could write a cookbook if the feature will be applied
Ready?
For review team