Extending extensions – solid modular concept

4 years ago

Filip Procházka
Moderator | 4692
+
+6
-

I've been using this for some time and I wanna propose standardization.

The idea is, that there should be a way that other extensions/modules can configure specific “parent” CompilerExtension. Let's look at example.

I have an extension Kdyby/Doctrine, that integrates Doctrine ORM into Nette. Doctrine must manage entity metadata and entities can be literary anywhere. In app/, libs/, in vendor/ in some other extension.

Say I wanna write blog extension based on Kdyby/Doctrine. How am I gonna tell the Doctrine extension, where to look for my entities, without requiring the user to copy some configuration to app/config.neon?

Let's create an interface IEntityProvider, that if the extension implements, Doctrine extension will look at and call the appropriate method, and act according to what is returned.

namespace KungfuBlog\DI;

class BlogExtension extends Nette\DI\CompilerExtension
    implements Kdyby\Doctrine\DI\IEntityProvider
{
    public function getEntityMappings()
    {
        return array(
            // namespace => directory
            'KungfuBlog' => __DIR__ . '/..'
        );
    }
}

When the application is compiled, I scan other extensions in Doctrine extension and look for interface IEntityProvider and process the returned configuration accordingly.

This is very automated and safe way of relying on “parent extension” to handle all the merging and configuring. And the most important part is, that anybody can start using it right now, and extensions users will no longer be required to connect extensions by hand.

I also propose following default interfaces

namespace Nette\Config\Extensions;

interface IRouterProvider
{
    /**
     * Returns array of ServiceDefinition,
     * that will be appended to setup of router service
     */
    function getRoutesDefinition();
}

interface IPresenterProvider
{
    /**
     * Returns array of ClassNameMask => PresenterNameMask
     * @see https://github.com/nette/nette/blob/master/Nette/Application/PresenterFactory.php#L138
     */
    function getPresenterMappings();
}


interface ITracyPanelsProvider
{
    /**
     * Returns array of panel renderer callbacks
     * @see https://doc.nette.org/cs/configuring#toc-debugger
     */
    function getTracyPanels();
}


interface ITracyBarPanelsProvider
{
    /**
     * Returns class or service that will be configured to bar panel
     * @see https://doc.nette.org/cs/configuring#toc-debugger
     */
    function getTracyBarPanel();
}

Mostly I wanna just the IPresenterProvider, but the others would be also nice.

There are plenty more where is will be a killer feature. Hey, wanna automate adding CSS of your new blog to the user? Simply depend on Webloader and implement interface Webloader\Nette\IAssetsProvider wink wink, and return directory path where your CSS files are, and bang, there are automatically added to your frontend and you havent had to configure or copy anything, just register the blog extension!

Also, I wanna propose you guys stop inventing new layers of abstraction above CompilerExtensions. They're fully capable of everything you need just now. Just use it and enjoy.

What do you guys think?

4 years ago

mishak
Member | 100
+
0
-

+1 except I need one free week to write ideas I have on paper and webloader will be history :)

IMO This should make it into 2.1 release because it will significantly boost packaging ecosystem around nette.

Last edited by mishak (2013-07-16 11:42)

4 years ago

Jan Jakeš
Member | 178
+
0
-

This looks great! Finally a complex solution to extensions that could make it to Nette because it seems to be a “Nette way”.

4 years ago

sifik
Member | 27
+
0
-

I like It! It looks more applicable than my proposal.

Is there some example of implementation basic router service for IRouterProvider?

I cannot wait! :-)

4 years ago

enumag
Member | 2128
+
0
-

I was not sure at first, but it looks very useful to me now. I'll give it a try.

One minor detail. I'd to change the scanning to this (would require small change in Nette):

foreach ($this->compiler->getExtensions('Kdyby\Doctrine\DI\IEntityProvider') as $extension) {
    $metadata = $extension->getEntityMappings();
    Validators::assert($metadata, 'array:1..');
    $config['metadata'] = array_merge($config['metadata'], $metadata);
}

4 years ago

Filip Procházka
Moderator | 4692
+
0
-

@enumag I hope that if this would be adopted, than little changes in api will continue, to make it more slick.

4 years ago

mishak
Member | 100
+
0
-

I put together package.

Before you use it understand that it will change without warning and when support for this is introduced to Nette its development will stop.
It might be possible in that case that compatibility package will be released but highly improbable as I got shit to do.

composer require rixxi/modular:@dev
source is at github

On the other side I find this approach very useful and this or similar interface will be highly used by other Rixxi packages. rixxi/user is one of them.

Last edited by mishak (2013-07-21 17:39)

4 years ago

sifik
Member | 27
+
0
-

@mishak Good job! :-)

I have the similar implementation of the same. Please look at my package Flame/modules (in README are more details about installation and how it works)

Unlike your @mishak's implementation, the Flame/Modules are working like Nette modules installer (I was very of bored of calling static methods for registration extensions in bootstrap.php) which support of this solid modular concept.

It's very simple and very fast.

I hope, you will love Flame/Modules :-)

Last edited by sifik (2013-07-22 10:29)

4 years ago

Filip Procházka
Moderator | 4692
+
0
-

“ModuleExtension” sounds to me like jabklové jablko, what about NamedExtension?

4 years ago

sifik
Member | 27
+
0
-

Filip Procházka wrote:

“ModuleExtension” mi zní jako “jabklové jablko”, co takhle NamedExtension?

Dobrý nápad. Díky, přejmenuji to.

//EDIT: WTF? Why is it translated?!

Last edited by sifik (2013-07-22 11:52)

4 years ago

frosty22
Member | 373
+
0
-

Oh YEAH! It sounds good ..

This is exactly what I need. I used Doctrine (Kdyby\Doctrine) too, and now I do it like COPY&PASTE between projects :(

It's a pitty that Nette prefer NDB, so this concept isn't useful for common nette, because this concept can provide a new possibilities of add-ons.

Last edited by frosty22 (2013-07-22 11:19)

4 years ago

mishak
Member | 100
+
0
-

Unlike your @mishak's implementation, the Flame/Modules are working like Nette modules installer (I was very of bored of calling static methods for registration extensions in bootstrap.php) which support of this solid modular concept.

Mine version is for Nette @dev only and therefore module registration should be done in extensions section of config. There is no easiest way then adding one line to config. I generally hate idea of non-explicit configuration of any sort.

Last edited by mishak (2013-07-22 11:57)

4 years ago

sifik
Member | 27
+
0
-

mishak wrote:

Mine version is for Nette @dev only and therefore module registration should be done in extensions section of config. There is no easiest way then adding one line to config. I generally hate idea of non-explicit configuration of any sort.

Flame/Modules are only working with @dev Nette too. :-)

But registration of modules cannot be register in extension (I tried it ;)). Modules must be registered before building of system container (just in bootstrap) because you don't have available all loaded extensions from method loadConfiguration() in specific extension and therefore e.g. IEntityProvider for Kdyby/Doctrine is not working.

//EDIT:
And the next problem is that if you will be register modules from extension, you cannot add some additional config files into system container :-)

Last edited by sifik (2013-07-22 12:12)

4 years ago

Filip Procházka
Moderator | 4692
+
0
-

@sifik config files added from modules are only different syntax for defining services. You shouldn't be configuring extensions back and forth, it will explode, I'm warning you.

And there is also no need to register extensions from extensions like this. If you wanna have automatic installation, have some app/config/extensions.neon and add lines to it using some composer installer or whatever.

The more complex you will make it, the more problems you will have. Keep it as flat as possible.

4 years ago

sifik
Member | 27
+
0
-

I agree! Config files are temporary solution until all providers are full supported or implemented.

Composer installer was very good idea. Well, I did the custom composer installer for nette modules. The installer will be adding (or removing) extension class into your extensions.(neon | php) file.

It's very flat ;-)

Last edited by sifik (2013-07-26 10:46)

4 years ago

sifik
Member | 27
+
0
-

Read more about my implementation on blog [CZE]

4 years ago

sifik
Member | 27
+
0
-

Added tests, examples & working demo for Flame/Modules! Brand new homepage http://flame-org.github.io/Modules/ :)

4 years ago

mishak
Member | 100
+
0
-

TL;DR Service tagging using heuristic, interfaces for resources (ie.: entities) are good enough. Defining presenter and routes this way is really awkward and doesn't work well.

I don't think this is much relevant after Nette 2.2.

Macros can be directly installed by extension without much hassle.

I am steering away from providing presenters since it adds too much burden, useless prefixes and related modules can't share same path so it comes to weird paths like basket.basket, shopOrder.order etc. (I guess this could be solved by mapping presenters differently).

Providing routes is just not good enough since there is no way how to prefix routes with general parameters without writing custom router (ie locale flag, translating, subdomains etc.). I decided to go around this awkwardness so I am working on a solution that will cover pretty url generation, parameters, route caching and allow resource & intent approach for linking content in application ie: <a n:intent="show $article"> or <a n:intent="show article $id">.

Not to be completely negative modular approach works really well for defining entities (and overwriting interface entities), overriding templates and for other resources. It just doesn't work well for core architecture.

Providing tracy panels and debug bars interface should be responsibility of tracy or nette bridge (finally after all these months). Someone please write a pull.

Also for registering latte filters (former template helpers) you can use this. I will add support for static helpers and config definition next week.

Generally I am working hard on getting rid of BasePresenters and other presenter unrelated bloat that goes with it and hopefully towards generated presenters that just hold components and models together and let them do the work. Decoupling templates from presenter in 2.2 helped a lot.

Last edited by mishak (2014-05-17 22:22)

4 years ago

Filip Procházka
Moderator | 4692
+
+2
-

Just FYI, on the last Nette Framework meetup, David agreed that my RFC should be added into Nette's extension.

If you have better idea how to register tracy and bar panels please write your own RFC, ideally before I start to implement it ;)

3 years ago

David Grudl
founder | 6706
+
0
-

@FilipProcházka Providers seem interesting, but how to implement them? Nearly all Tracy panels are loaded, when their domain is loaded. Dibi\NDB panel is loaded, when connection is established. Session panel is loaded, when session is opened. Etc.

3 years ago

xificurk
Member | 118
+
0
-

As I've been poking around nette/bootstrap#4 I realized this can work well only for really core features in the current component-based status quo.

Take for example Tracy (bar) panels.
Where would you put the provider interface? I guess into Tracy, right? OK, you've just introduced a hard dependency on Tracy to any component that wants to be able to provide Tracy panels – not good at all.
OK, next idea… the interface could be placed into some really core package. Like utils? Nonsense, why would the provider interface be in a package with no provider and no consumer?
OK, next idea… how about a new package for the interface – kind of works, but it's really weird to have a package with only one interface.
OK, next idea… how about reversing the relation and instead of providers introduce consumers? That could work, but the only reason to have for this a consumer interface is the possibility to replace the default consumers by our own. Well, then your back to square one – in order to replace the consumer you need to depend on their package to get the interface.

I would suggest this: If you expect that your extension will be configured from other extensions, you should provide a public API for that (within the extension class). So, other authors won't need to directly mess around with the configuration of your services, they simply check if your extension is enabled and use its API. Examples for this might be TracyExtension (does not exist yet), LatteExtension, WebLoader, Translators, …

3 years ago

Filip Procházka
Moderator | 4692
+
0
-

Another example here: https://github.com/…te-di/pull/1

You must put the interface next to extension that can be configured with the given provider. Then if you require the extension, you can implement the provider interface to your extension and it just works.

2 years ago

JuniorJR
Member | 181
+
0
-

Guys, how do you solve the problem, when some INiceThingProvider extends Nette\DI\CompilerExtension but needs some extra dependencies to generate actual return values in INiceThingProvider::someMethod()? For example Nette\Security\User or Nette\Localization\ITranslator.

use Nette\DI\CompilerExtension;
use Nette\Security\User;

class MyExtension extends CompilerExtension implements \INiceThingProvider
{
    // this kind of dependency is bad :(
    public function __construct(User $user)
    {
        $this->user = $user;
    }

    /**
     * INiceThingProvider::coolMethod()
     *
     * @return array
     */
    public function coolMethod()
    {
        return [
            'title' => $this->user->identity->name,
            ...
        ];
    }

    public function beforeCompile()
    {
        foreach ($this->compiler->getExtensions('INiceThingProvider') as $extension) {
            $extension->coolMethod();
            // add services, setups etc.
        }
    }
}

I like the solid modular concept but it seems that in this case, MyExtension itself cannot be INiceThingProvider but another class needs to be introduced to accomplish the desired result but in that case, it would not be CompilerExtension so it would not be accesible via Compiler::getExtensions($type) method…

Last edited by JuniorJR (2015-06-30 21:53)

2 years ago

Michal Vyšinský
Member | 614
+
+3
-

The example you provided does not make sense. CompilerExtension is always executed only once, when container is being compiled. You should depend only on configuration passed from neon in all your CompilerExtensions.

2 years ago

JuniorJR
Member | 181
+
0
-

@MichalVyšinský I think it makes sense, I do some stuff using ContainerBuilder like adding services and setups depending on what returns INiceThingProvider::coolMethod() method. The only solution is add separate service which will implement the logic, isn't it?

Last edited by JuniorJR (2015-06-30 22:09)

2 years ago

enumag
Member | 2128
+
+2
-

Complier extension is not service so you can't inject services at all because DIC does not exist yet. Try being more concrete with what you're trying to do. And also create a new topic please, it is not related to this RFC.

Last edited by enumag (2015-06-30 22:12)