Extending extensions – solid modular concept
- Filip Procházka
- Moderator | 4668
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?
- sifik
- Member | 27
I like It! It looks more applicable than my proposal.
Is there some example of implementation basic router service for IRouterProvider?
I cannot wait! :-)
- enumag
- Member | 2118
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);
}
- Filip Procházka
- Moderator | 4668
@enumag I hope that if this would be adopted, than little changes in api will continue, to make it more slick.
- mishak
- Member | 94
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)
- sifik
- Member | 27
@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)
- Filip Procházka
- Moderator | 4668
“ModuleExtension” sounds to me like jabklové jablko, what about
NamedExtension
?
- frosty22
- Member | 373
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)
- mishak
- Member | 94
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)
- sifik
- Member | 27
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)
- Filip Procházka
- Moderator | 4668
@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.
- sifik
- Member | 27
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)
- sifik
- Member | 27
Added tests, examples & working demo for Flame/Modules! Brand new homepage http://flame-org.github.io/Modules/ :)
- mishak
- Member | 94
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)
- Filip Procházka
- Moderator | 4668
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 ;)
- David Grudl
- Nette Core | 8228
@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.
- xificurk
- Member | 121
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, …
- Filip Procházka
- Moderator | 4668
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.
- JuniorJR
- Member | 181
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)
- Michal Vyšinský
- Member | 608
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.
- JuniorJR
- Member | 181
@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)