Skip to content
This repository has been archived by the owner on Jan 30, 2020. It is now read-only.

Make StandaloneExtensionManagers configurable #55

Closed
wants to merge 22 commits into from

Conversation

Synchro
Copy link
Contributor

@Synchro Synchro commented Nov 10, 2017

This is a simple PR that fixes my request in #54. If you're happy with this, I will add changes to the docs as well, also fixing #44.

@Xerkus
Copy link
Member

Xerkus commented Nov 10, 2017

👎 from me. Standalone extension manager should not be extended and docs should be corrected not to suggest that.

@Synchro
Copy link
Contributor Author

Synchro commented Nov 10, 2017

Rationale?

@Synchro Synchro closed this Nov 10, 2017
@weierophinney
Copy link
Member

@Xerkus I disagree. In this case, extension will allow users to not need a container implementation in order to provide more extensions; they can instead extend the standalone implementation to add theirs in.

@Synchro — I'm re-opening and will review.

@Xerkus
Copy link
Member

Xerkus commented Nov 13, 2017

I still oppose extending the class. It have numerous downsides.

  • extended class will not be updated along with base StandaloneExtensionManager if it copied the property.
  • extended class that is changing behavior of StandaloneExtensionManager should be separate implementation anyway as it will be a coupling of unrelated responsibilities.
  • extending limits our ability to change implementation details at will, which is an additional maintenance burden. Not as much of a problem in this simple isolated case but it is there.

I see only two valid options here and none of them is to extend:

  • introduce methods to configure extensions property to the StandaloneExtensionManager and forbid extension
  • extend functionality via decorator implementing the interface, as I was saying in the related Issue:
final class MyExtensionManager implements ExtensionManagerInterface
{
    private $decoratedManager;

    private $extensions = [
        'Media\Entry' = MyApp\RSS\Media\Entry::class,
    ];

    public function __construct(ExtensionManagerInterface $em)
    {
        $this->decoratedManager = $em;
    } 

    /**
     * Do we have the extension?
     *
     * @param  string $extension
     * @return bool
     */
    public function has($extension)
    {
        return (array_key_exists($extension, $this->extensions) || $this->decoratedManager->has($extension));
    }

    /**
     * Retrieve the extension
     *
     * @param  string $extension
     * @return mixed
     */
    public function get($extension)
    {
        if (array_key_exists($extension, $this->extensions)) {
            $class = $this->extensions[$extension];
            return new $class();
        }
        return $this->decoratedManager->get($extension);
    }
}

And of course there is always configurable container implementation for more complex needs.

@Synchro
Copy link
Contributor Author

Synchro commented Nov 14, 2017

All this seems to be massive overkill - something that calls itself a plugin manager singularly fails to do anything matching that description - needing to write a convoluted decorator at all smells like a workaround for an inflexible base class. If a provided extension manager doesn't manage extensions, what is it for? A simpler alternative to extend/decorate would be to add an "addExtension" method, so I could say:

$m = new StandalonePluginManager;
$m->addExtension('Media/Entry', 'MyApp\RSS\Media\Entry');

I'm using a framework because it helps me do things with less effort; I want to add one extension, but I have zero interest in writing an extension manager - that's a role I would expect the framework to fulfil.

@Xerkus
Copy link
Member

Xerkus commented Nov 14, 2017

Extension manager is a contract established by the interface. Responsibility of the contract is to manage extensions for zend-feed, ie create, configure and provide.
StandaloneExtensionManager is a concrete implementation that is in no way obligated to provide anything beside said contract. Its responsibility is to make zend-feed work standalone without external dependency and it does exactly that.
NoopExtensionManager, for example, would do nothing, provide no extensions at all while still being a valid concrete implementation.

If you need flexible configurable extension manager, that role is filled by ExtensionPluginManager. It is already written and supported.

As I said in previous comment, I will support expanding standalone responsibilities by making it configurable. Feel free to open PR for that.

@@ -9,9 +9,9 @@

namespace Zend\Feed\Reader;

class StandaloneExtensionManager implements ExtensionManagerInterface
final class StandaloneExtensionManager implements ExtensionManagerInterface
Copy link
Member

Choose a reason for hiding this comment

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

We can't make it final, it will require a major version bump as someone is quite likely already extending it.

@Xerkus Xerkus changed the base branch from master to develop November 14, 2017 07:42
@Xerkus
Copy link
Member

Xerkus commented Nov 14, 2017

Can you also provide same change for Writer?

@Synchro
Copy link
Contributor Author

Synchro commented Nov 14, 2017

OK. I've removed final. Are you happy with the general approach?

* Remove an extension.
*
* @param string $name
* @return bool
Copy link
Member

Choose a reason for hiding this comment

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

It does not need to report the result. It is idempotent operation. void would be fine

@Synchro
Copy link
Contributor Author

Synchro commented Nov 14, 2017

I've noticed that the code style of the extension list is quite different in each. In Reader:

    'Atom\Entry'            => 'Zend\Feed\Reader\Extension\Atom\Entry',

in Writer:

    'Atom\Renderer\Feed'           => Extension\Atom\Renderer\Feed::class,

They are consistent within themselves, but not with each other. It makes writing tests easier using the string style as I don't have to provide a mocked extension class - I know that older versions used the literal style, so I assume there's some history to that.

I noticed that the Writer doesn't have a test file for this, so I'm adding one.

@Xerkus
Copy link
Member

Xerkus commented Nov 14, 2017

::class pseudoconstant is preferred. It is a special case, it does not require class to actually exist.
What it does is resolves class using namespace and local imports, returns string.
\SomeClass::class and 'SomeClass' are exactly the same, except static analysis will warn of non-existent class in former and won't in latter.

@Synchro
Copy link
Contributor Author

Synchro commented Nov 14, 2017

Hm. Looks like it fails to find classes using pseudoconstants:

Fatal error: Class 'Zend\Feed\Reader\Zend\Feed\Reader\Extension\Podcast\Feed' not found in /home/travis/build/zendframework/zend-feed/src/Reader/StandaloneExtensionManager.php on line 50

I will switch back to strings.

@Synchro
Copy link
Contributor Author

Synchro commented Nov 14, 2017

Uh, I could always just un-break the fully namespaced names...

'Syndication\Feed' => 'Zend\Feed\Reader\Extension\Syndication\Feed',
'Thread\Entry' => 'Zend\Feed\Reader\Extension\Thread\Entry',
'WellFormedWeb\Entry' => 'Zend\Feed\Reader\Extension\WellFormedWeb\Entry',
'Atom\Entry' => Zend\Feed\Reader\Extension\Atom\Entry::class,
Copy link
Member

Choose a reason for hiding this comment

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

It is resolved to FQCN using normal rules.
Extension\Atom\Entry::class will resolve properly to Zend\Feed\Reader\Extension\Atom\Entry

@Xerkus Xerkus changed the title Make $extensions property protected Make StandaloneExtensionManagers configurable Nov 14, 2017
@Xerkus
Copy link
Member

Xerkus commented Nov 14, 2017

Can you also update docs? For this change only

@Xerkus Xerkus added this to the 2.9.0 milestone Nov 14, 2017
@Synchro
Copy link
Contributor Author

Synchro commented Nov 14, 2017

Fixing test data provider...

namespace ZendTest\Feed\Writer;

use PHPUnit\Framework\TestCase;
use Zend\Feed\Reader\StandaloneExtensionManager;
Copy link
Member

Choose a reason for hiding this comment

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

This is supposed to be Writer

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Ah, my IDE had collapsed the use statements so I didn't see them. Fixed.

*/
public function remove($name)
{
if (array_key_exists($name, $this->extensions)) {
Copy link
Member

Choose a reason for hiding this comment

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

Any reason, why this check is removed?

Copy link
Member

Choose a reason for hiding this comment

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

it is not needed. unset will work the same and there is no distinction between removing instance and noop.

Copy link
Member

Choose a reason for hiding this comment

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

@Xerkus
You are right, it throws no error.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

It's because unset is a language construct, not a real function, and it doesn't care whether the thing you're unsetting exists or not. The check was only there before to set the return value.

*/
public function add($name, $class)
{
$this->extensions[$name] = $class;
Copy link
Member

Choose a reason for hiding this comment

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

No validation what is added to the manager?

Copy link
Member

Choose a reason for hiding this comment

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

good point. $class should be checked to be instance of Zend\Feed\Reader\Extension\AbstractEntry or Zend\Feed\Reader\Extension\AbstractFeed

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I wondered about that - $class can be a string (it was in the components until I changed it), but it might be the name of something that doesn't exist yet, so I don't know that it can be validated at this point.

Copy link
Member

@Xerkus Xerkus Nov 14, 2017

Choose a reason for hiding this comment

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

with check it will be autoloaded when extension is registered. Plugin manager validates on creation.

@Xerkus
Copy link
Member

Xerkus commented Nov 14, 2017

This check for Writer extensions makes little sense

public function validate($plugin)
{
if ($plugin instanceof Extension\AbstractRenderer) {
// we're okay
return;
}
if ('Feed' == substr(get_class($plugin), -4)) {
// we're okay
return;
}
if ('Entry' == substr(get_class($plugin), -5)) {
// we're okay
return;
}
throw new InvalidServiceException(sprintf(
'Plugin of type %s is invalid; must implement %s\Extension\RendererInterface '
. 'or the classname must end in "Feed" or "Entry"',
(is_object($plugin) ? get_class($plugin) : gettype($plugin)),
__NAMESPACE__
));
}

How should it be ported to standalone extension manager?

@Synchro
Copy link
Contributor Author

Synchro commented Nov 14, 2017

All passing now. That OK?

@@ -9,6 +9,8 @@

namespace Zend\Feed\Writer;

use Zend\ServiceManager\Exception\InvalidServiceException;
Copy link
Member

Choose a reason for hiding this comment

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

You cannot use ServiceManager exceptions in here. Use Zend\Feed\Writer\Exception\InvalidArgumentException

@@ -9,22 +9,24 @@

namespace Zend\Feed\Reader;

use Zend\ServiceManager\Exception\InvalidServiceException;
Copy link
Member

Choose a reason for hiding this comment

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

You cannot use ServiceManager exceptions in here. Use Zend\Feed\Reader\Exception\InvalidArgumentException

Copy link
Contributor Author

Choose a reason for hiding this comment

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

But that's the type that is thrown?

Copy link
Member

Choose a reason for hiding this comment

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

throw that other type

Copy link
Contributor Author

Choose a reason for hiding this comment

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

OK, I see why.

@@ -80,4 +85,13 @@ public function testEachPluginRetrievalReturnsNewInstance($pluginName, $pluginCl
$this->assertInstanceOf($pluginClass, $test);
$this->assertNotSame($extension, $test);
}

public function testPluginAddRemove()
Copy link
Member

Choose a reason for hiding this comment

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

You need additional tests for expected exception in case of invalid extension class or object of valid extension.

Copy link
Member

Choose a reason for hiding this comment

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

You need additional tests to ensure extensions derived from both allowed classes are accepted

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I've added tests for the exceptions, but the check in add is already checking for derived classes, because the class it checks for is abstract, so anything that is_a Extension\AbstractEntry is by definition derived from it, so passing a further derivation wouldn't be testing anything further.

Copy link
Member

Choose a reason for hiding this comment

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

Those should be separate tests with descriptive names for what you are testing for
testAddAcceptsValidExtensionClasses()
testAddRejectsInvalidExtensions()
testAddRejectsInstanceOfValidExtension()

That way we have intent of the test and its implementation details rather than just asserting that current code runs without errors. It makes sure that if code changes at a later stage no intent will be lost.

$this->assertNotSame($extension, $test);
}

public function testPluginAddRemove()
Copy link
Member

Choose a reason for hiding this comment

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

Same here, additional tests for expected exception in case of invalid extension class or object of valid extension.

Copy link
Member

Choose a reason for hiding this comment

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

additional tests to ensure extensions derived from allowed class or with classnames ending with 'Feed' or 'Entry' are accepted

weierophinney added a commit that referenced this pull request Dec 4, 2017
Make StandaloneExtensionManagers configurable
weierophinney added a commit that referenced this pull request Dec 4, 2017
weierophinney added a commit that referenced this pull request Dec 4, 2017
@weierophinney
Copy link
Member

Thanks, @Synchro

Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Projects
None yet
Development

Successfully merging this pull request may close these issues.

4 participants