-
-
Notifications
You must be signed in to change notification settings - Fork 3.7k
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
[4.0] Move component categories helper to services #19667
Conversation
Honestly I think that we keep ContentCategories here - the purpose of this was to remove guessing of path location - whilst still having the class - not removing the class altogether. So can we add it back for now please? Even if we end up removing it in a future refactor I want to figure out how it looks in this format :) |
|
||
include_once $path; | ||
} | ||
$categories = ComponentHelper::boot($parts[0])->getCategories(count($parts) > 1 ? $parts[1] : ''); |
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.
👍
* | ||
* @since __DEPLOY_VERSION__ | ||
*/ | ||
public function getCategories($section = ''): Categories |
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.
You can't do this return typehint - it needs to be nullable (i.e. ?Categories
) but it's only available in PHP 7.1 http://php.net/manual/en/migration71.new-features.php - so we'll have to go without one with a PHP 7.0 minimum
You mean in a namespaced way or the old way? |
Well, a component with its own container doesn't have to follow a strict class naming convention (because the container will define where the service lives), whereas stuff using the legacy container would need to follow the old way. So, I think you could give it a properly namespaced class name since you're creating a container for com_content. With that said though, since we support options setting in the constructor, we need to ensure the categories service isn't singleton in the container (i.e. it can't be a shared service). Otherwise you'll always get an object with the first object's options. |
Added a content specific service class in 1a119fd. |
I'v added an interface and trait to load the extensions and added it to the application. |
@@ -389,4 +362,43 @@ protected function _load($id) | |||
$this->_nodes[$id] = null; | |||
} | |||
} | |||
|
|||
/** | |||
* Sets the options for this service. The given options do overwrite the |
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.
pedantic I know but the word "do" should be deleted
return; | ||
} | ||
|
||
// Merge the options to not loose the base config |
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.
lose not loose
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.
There's one more thing to figure out here. Which is what does an extension do if it doesn't support categories. Does it's container getCategories
method return null
all the time. Or do we do some sort of NotSupported
Exception type for these things rather than returning null?
* | ||
* @since __DEPLOY_VERSION__ | ||
*/ | ||
public function setOptions(array $options) |
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.
Can we move this back to the constructor please. I know this is a bit simpler to view but I risking people call setOptions
and reprovisioning just seems vaguely boring. Let's do one risky thing at a time
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 had to do it that way as in the ComponentContainer here , it is not possible to pass the options. If there is a way to set the options, then I can change it back to the constructor.
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.
You make this an anonymous function like we do in all the service providers for the main DI Container https://github.com/joomla/joomla-cms/pull/19667/files#diff-a56b3bb3afcd610a7b1560ed05d8a146R17
*/ | ||
trait ExtensionLoader | ||
{ | ||
use ContainerAwareTrait; |
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 thought best practice on this stuff was rather explicitly including a trait here was to add an abstract method into the trait and then ContainerAwareTrait
into the Application thing.
At the moment it returns null. If we throw an exception, we always have to do a try/catch around it which would be for me overhead. I was even thinking about to return an empty array just to not always write another "if" check around the for loop. |
return $container->get($serviceName); | ||
} | ||
|
||
$path = $extensionPath . '/services/services.php'; |
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.
Personally I'd suggest a proper PHP class gets used over a procedural file receiving variables from a calling method's scope.
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.
The idea behind this is to have something similar like the service declaration in Symfony. At the moment it is PHP only, but perhaps we add later support for service declaration trough YSON or YAML.
Another benefit is that we do not have to do some magic class compiling again. We can then even get rid of the dispatcher file and load the dispatcher trough the component container. That's my next plan when this one gets merged.
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.
Symfony has a different style container than we do (they have a compiled container which is cached and built for production using code generators, we have a runtime container built through injected callables, which when they are Closures can't be serialized/cached in any sane way). So until we move our code to support compiled/cached containers like that, I don't think we need to consider possible support for other formats.
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 now we should not focus too much on the service loading strategy. We can change that afterwards when I did the Dispatcher service pr. Is the rest ok?
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.
Just add the dispatcher into this one, then add the service loading logic we discussed in Glip and I'll get it merged
Changed the service registering to a class. I would like to do the dispatcher in it's own pr. |
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.
The ExtensionManager(Interface|Trait)
looks more like a ComponentFactory
to me...
* @copyright Copyright (C) 2005 - 2017 Open Source Matters, Inc. All rights reserved. | ||
* @license GNU General Public License version 2 or later; see LICENSE.txt | ||
*/ | ||
|
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.
No namespace?
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 is the only thing which get loaded without the namespace. In a first version it was just a plain php file which registered services in a container as you can see here.
@@ -6,15 +6,18 @@ | |||
* @copyright Copyright (C) 2005 - 2017 Open Source Matters, Inc. All rights reserved. | |||
* @license GNU General Public License version 2 or later; see LICENSE.txt | |||
*/ | |||
namespace Joomla\Component\Content\Site\Service; |
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.
Services should be application independent. So why ...\Site\...
?
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.
It can even be removed, but for now George wants to have it in place, see the 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'm fine with the class itself so far - I just don't see, why it is in a namespace that semantically restricts it to 'site'.
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 have application agnostic component code. So we'd need a solution to that to get around that (mildly important in some ways) semantic detail.
* @param string $extensionName The extension name | ||
* @param string $extensionPath The path of the extension | ||
* | ||
* @return mixed The extension |
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.
In self::bootComponent()
you rely on the return value being a ComponentInterface
. In general, mixed
should never be used as return type.
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 had an Interface ExtensionInterface before but removed it because it would be more of a marker interface without any function for now.
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.
Nevertheless, the return type should be ComponentInterface
as expected by bootComponent()
, or you need to ensure the right type in bootComponent()
- that's the price of mixed
.
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.
On a long term I have in mind to support multiple extension types. That's why mixed. I understand your argument.
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.
If you want to keep mixed
(which is ok for now), you need to check that you actually got a ComponentInterface
complying object, like
public function bootComponent($component): ComponentInterface
{
// ...
$extension = $this->loadExtension('component', $component, $path);
if ($extension instanceof ComponenInterface)
{
return $extension;
}
throw new UnexpectedExtensionType("$component is not a component");
}
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.
The return type declaration does it for us ;)
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.
Questions are answered so far, just one thing left.
* @param string $extensionName The extension name | ||
* @param string $extensionPath The path of the extension | ||
* | ||
* @return mixed The extension |
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.
If you want to keep mixed
(which is ok for now), you need to check that you actually got a ComponentInterface
complying object, like
public function bootComponent($component): ComponentInterface
{
// ...
$extension = $this->loadExtension('component', $component, $path);
if ($extension instanceof ComponenInterface)
{
return $extension;
}
throw new UnexpectedExtensionType("$component is not a component");
}
I think there's still quite a lot of tweaks to be made here - but this will do as a starting point :) |
Pull Request for Issue #19580.
Summary of Changes
Introduces a boot functionality for components where they can register services in a container instead of providing helper classes which get loaded magically. Consumer of these services can then boot a component first to make sure, that the services are loaded and get an instance of
ComponentInterface
.On a long term, all the extensions should be bootable and provide an interface to their environment.
Testing Instructions
The instructions below do test the old
JCategories::getInstance()
code and the new one by booting the component.Expected result
All should work as before.
Actual result
All should work as before.
Documentation Changes Required
The new service based approach needs to be documented.