Skip to content
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

Merged
merged 30 commits into from
Mar 1, 2018

Conversation

laoneo
Copy link
Member

@laoneo laoneo commented Feb 13, 2018

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.

  • Create a second category for articles
  • Create an article custom field
  • Open an article on the front end

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.

@wilsonge
Copy link
Contributor

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] : '');
Copy link
Contributor

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
Copy link
Contributor

@wilsonge wilsonge Feb 13, 2018

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

@laoneo
Copy link
Member Author

laoneo commented Feb 13, 2018

So can we add it back for now please

You mean in a namespaced way or the old way?

@mbabker
Copy link
Contributor

mbabker commented Feb 13, 2018

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.

@laoneo
Copy link
Member Author

laoneo commented Feb 13, 2018

Added a content specific service class in 1a119fd.

@laoneo
Copy link
Member Author

laoneo commented Feb 14, 2018

I'v added an interface and trait to load the extensions and added it to the application.

@laoneo laoneo changed the title [4.0][WIP] Move component categories helper to services [4.0] Move component categories helper to services Feb 14, 2018
@laoneo
Copy link
Member Author

laoneo commented Feb 14, 2018

Removed the WIP. Would be good if @mbabker and @wilsonge can review it again. If all ok then I can move to the other services.

@@ -389,4 +362,43 @@ protected function _load($id)
$this->_nodes[$id] = null;
}
}

/**
* Sets the options for this service. The given options do overwrite the
Copy link
Contributor

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
Copy link
Contributor

Choose a reason for hiding this comment

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

lose not loose

Copy link
Contributor

@wilsonge wilsonge left a 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)
Copy link
Contributor

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

Copy link
Member Author

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.

Copy link
Contributor

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;
Copy link
Contributor

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.

@laoneo
Copy link
Member Author

laoneo commented Feb 14, 2018

Or do we do some sort of NotSupported Exception type for these things rather than returning null?

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';
Copy link
Contributor

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.

Copy link
Member Author

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.

Copy link
Contributor

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.

Copy link
Member Author

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?

Copy link
Contributor

@wilsonge wilsonge left a 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

@laoneo
Copy link
Member Author

laoneo commented Feb 19, 2018

Changed the service registering to a class. I would like to do the dispatcher in it's own pr.

Copy link
Member

@nibra nibra left a 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
*/

Copy link
Member

Choose a reason for hiding this comment

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

No namespace?

Copy link
Member Author

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;
Copy link
Member

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\...?

Copy link
Member Author

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.

Copy link
Member

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'.

Copy link
Contributor

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
Copy link
Member

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.

Copy link
Member Author

@laoneo laoneo Feb 21, 2018

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.

Copy link
Member

@nibra nibra Feb 21, 2018

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.

Copy link
Member Author

@laoneo laoneo Feb 21, 2018

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.

Copy link
Member

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");
}

Copy link
Member Author

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 ;)

Copy link
Member

@nibra nibra left a 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
Copy link
Member

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");
}

@wilsonge
Copy link
Contributor

wilsonge commented Mar 1, 2018

I think there's still quite a lot of tweaks to be made here - but this will do as a starting point :)

@wilsonge wilsonge merged commit b0bbab1 into joomla:4.0-dev Mar 1, 2018
@wilsonge wilsonge deleted the j4/boot branch March 1, 2018 21:23
@wilsonge wilsonge added this to the Joomla 4.0 milestone Mar 1, 2018
This was referenced May 23, 2018
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

6 participants