Using StructureMap 3 in multiple ASP MVC projects - asp.net

First lets start to describe my project architecture.
I have a asp.net mvc application called Portal.Web as startup project and multiple asp.net mvc applications which called Plugin.XXX (Plugin.News, Plugin.Cms, etc) and Portal.Web has references from all of these plugins.
I installed StrcutureMap.Mvc5 for each one of these Plugins
As you know, when you install StructureMap a folder created called DependencyResolution and it contains some files, one of them is IoC.cs where you can initialize your container.
Now my problem is since all plugins has their own IoC.cs, it seems like they override each other containers so at the end I get this error :
No parameterless constructor defined for this object
// inner exception
No default Instance is registered and cannot be automatically determined for type 'Portal.Plugins.Page.Interfaces.IPage'
There is no configuration specified for Portal.Plugins.Page.Interfaces.IPage
1.) new RouteController(*Default of IUnitOfWork*, *Default of IPage*)
2.) Portal.Web.Controllers.RouteController
3.) Instance of Portal.Web.Controllers.RouteController
4.) Container.GetInstance(Portal.Web.Controllers.RouteController)
There is no configuration specified for Portal.Plugins.Page.Interfaces.IPage
I could use ONE IoC.cs in Portal.Web project but I need to keep modularity and make Plugins independent as much as possible.
is there any way to keep plugins containers independent?
namespace Portal.Plugins.Account.DependencyResolution {
using StructureMap;
public static class IoC {
public static IContainer Initialize() {
return new Container(c =>
{
c.AddRegistry<DefaultRegistry>();
c.For<IUnitOfWork>().LifecycleIs(new HttpContextLifecycle()).Use<AccountDbContext>();
c.For<IAccount>().Use<AccountService>();
});
}
}
}
namespace Portal.Plugins.Cms.DependencyResolution {
using StructureMap;
public static class IoC {
public static IContainer Initialize() {
return new Container(c =>
{
c.AddRegistry<DefaultRegistry>();
c.For<IUnitOfWork>().LifecycleIs(new HttpContextLifecycle()).Use<CmsDbContext>();
c.For<IPage>().Use<PageService>();
c.For<IMenu>().Use<MenuService>();
c.For<IMedia>().Use<MediaService>();
});
}
}
}

If I understand your problem correctly, couldn't you create a DependencyResolution class library that contains the StructureMap.Mvc5 package that's then used throughout your plugins (this saves each of your plugins containing versions of StructureMap.Mvc5) and having a single reference to your DependencyResolution class library.
From here your individual plugins can then contain their own StructureMap registries, with more global registries being kept in the DependencyResolution class library.

Related

Extend and override default lighthouse directive

Is there anyway that I can override a directive like: src/Schema/Directives/WhereDirective.php for instance this doesn't support some methods on my custom builder, I know I can make another directive and extend this like #myWhere but that's dirty, would be nice to be able to override the #where itself.
I've searched around but nothing was found about this sadly!
I edit my composer.json and manipulate the class mappings. In this example, I wanted to override some cache classes.
"autoload": {
"psr-4": {
"App\\": "app/",
"Database\\Factories\\": "database/factories/",
"Database\\Seeders\\": "database/seeders/",
"Nuwave\\Lighthouse\\Cache\\": "lighthouseV6/cache/"
},
"exclude-from-classmap": [
"vendor/nuwave/lighthouse/src/Cache/CacheKeyAndTags.php",
"vendor/nuwave/lighthouse/src/Cache/CacheKeyAndTagsGenerator.php",
"vendor/nuwave/lighthouse/src/Cache/CacheDirective.php"
]
},
Then create a folder "lighthouseV6/cache" in the root of the project and copy the classes I wanted to override from "vendor/nuwave/lighthouse/src/Cache" inside it.
I found the solution. according to https://lighthouse-php.com/5/custom-directives/getting-started.html#register-directives
When Lighthouse encounters a directive within the schema, it starts looking for a matching class in the following order: 1. User-defined namespaces as configured in config/lighthouse.php, defaults to App\GraphQL\Directives 2. The RegisterDirectiveNamespaces event is dispatched to gather namespaces defined by plugins, extensions or other listeners 3. Lighthouse's built-in directive namespace.
So it did seem like override could be possible, and it was.
I haven't tried first method (App\GraphQL\Directive...) but that probably would work too, I went with the second method the RegisterDirectiveNamespaces event, since I was writing a package.
Make all your directives in the same folder under one namespace eg:
namespace SteveMoretz\Something\GraphQL\Directives;
Now in a service provider (Can be your package's service provider or AppServiceProvider or any service provider you get the idea.) register that namespace your directives are under.
use Illuminate\Contracts\Events\Dispatcher;
use Nuwave\Lighthouse\Events\RegisterDirectiveNamespaces;
class ScoutGraphQLServiceProvider {
public function register(Dispatcher $dispatcher) {
$dispatcher->listen(
RegisterDirectiveNamespaces::class,
static function (): string {
return "SteveMoretz\Something\GraphQL\Directives";
}
);
}
}
That's it so for an example I have overridden the #where directive, first I created a file named as original WhereDirective.php then put these contents in it:
<?php
namespace SteveMoretz\Something\GraphQL\Directives;
use Nuwave\Lighthouse\Scout\ScoutBuilderDirective;
use Nuwave\Lighthouse\Support\Contracts\ArgBuilderDirective;
use Nuwave\Lighthouse\Schema\Directives\BaseDirective;
use Nuwave\Lighthouse\Schema\Directives\WhereDirective as WhereDirectiveOriginal;
use Nuwave\Lighthouse\Support\Contracts\FieldResolver;
class WhereDirective extends WhereDirectiveOriginal
{
public function handleBuilder($builder, $value): object
{
$clause = $this->directiveArgValue('clause', 'where');
// do some other stuff too... my custom logic
return $builder->{$clause}(
$this->directiveArgValue('key', $this->nodeName()),
$this->directiveArgValue('operator', '='),
$value
);
}
}
Now whenever we use #where my custom directive runs instead of the original one, but be careful what you do in this directive don't alter the whole directive try to extend the original and add more options to it, otherwise you would end up confusing yourself later!

Load Symfony (5.2) config from database

I am a newbie in Symfony but I know how to use OOP in PHP.
I try (with frustration) to couple custom parameters with Symfony configs by using Doctrine entities.
To solve the problem I used for e.g. the answer from Michael Sivolobov: https://stackoverflow.com/a/28726681/2114615 and other sources.
My solution:
Step 1: Create new package in config folder
-> config
-> packages
-> project
-> services.yaml
-> project
-> src
-> ParameterLoaderBundle.php
-> DependencyInjection
-> Compiler
-> ParameterLoaderPass.php
Step 2: Import the new resource
# config/services.yaml
...
imports:
- { resource: 'packages/project/config/services.yaml' }
...
Step 3: Package coding
# packages/project/config/services.yaml
services:
Project\:
resource: "../src"
<?php
namespace Project;
use Project\DependencyInjection\Compiler\ParameterLoaderPass;
use Symfony\Component\DependencyInjection\Compiler\PassConfig;
use Symfony\Component\DependencyInjection\ContainerBuilder;
use Symfony\Component\HttpKernel\Bundle\Bundle;
class ParameterLoaderBundle extends Bundle
{
public function build(ContainerBuilder $container)
{
parent::build($container);
$container->addCompilerPass(new ParameterLoaderPass(), PassConfig::TYPE_AFTER_REMOVING);
}
}
<?php
namespace Project\DependencyInjection\Compiler;
use App\Entity\SettingCategory;
use Symfony\Component\DependencyInjection\Compiler\CompilerPassInterface;
use Symfony\Component\DependencyInjection\ContainerBuilder;
class ParameterLoaderPass implements CompilerPassInterface
{
public function process(ContainerBuilder $container)
{
$em = $container->get('doctrine.orm.default_entity_manager');
$setting = $em->getRepository(SettingCategory::class)->findAll();
$container->setParameter('test', $setting);
}
}
After at all I test the new Parameter in my API controller:
$this->getParameter('Test');
But the following error message appears:
The parameter \"test\" must be defined.
Couple of things going on here. First off, loading config from a database is very unusual in Symfony so it is not surprising that you are having difficulty. Secondly, your process code is never getting called. Part of debugging is making sure that code that you expect to be called is in fact being called. Third, you really got off on a tangent with attempting to add a bundle under config. Way back in Symfony 2 there used to be more bundle related stuff under app/config and it may be that you discovered some old articles and misunderstood them.
But, the big problem here is that Symfony has what is known as a 'compile' phase which basically processes all the configuration and caches it. Hence the CompilerPassInterface. Unfortunately, services themselves are not available during the compile phase. They simply don't exist yet so no entity manager. You need to open your own database connection if you really want to load config from a database. You will want to use just a database connection object and not the entity manager since part of the compile phase is to process the entities themselves.
So get rid of all your code and just adjust your Kernel class:
# src/Kernel.php
class Kernel extends BaseKernel implements CompilerPassInterface
{
use MicroKernelTrait;
public function process(ContainerBuilder $container)
{
$url = $_ENV['DATABASE_URL'];
$conn = DriverManager::getConnection(['url' => $url]);
$settings = $conn->executeQuery('SELECT * FROM settings')->fetchAllAssociative();
$container->setParameter('test',$settings);
}
And be aware that even if you get all this working, you will need to manually rebuild the Symfony cache after updating your settings table. It is not going to be automatic. You really might consider taking a completely different approach.

How to release or distribute an application that uses mikro-orm?

In the configuration I have to specify the paths to .js and .ts files defining entities:
MikroORM.init({
...
entitiesDirs: ["build/entities"],
entitiesDirsTs: ["src/entities"],
});
So, when I will go to release or distribute the application. Will I need distribute the typescript code too? or will I need distribute only the cache generated? or will I need distribute both? or... none?
As of MikroORM v2.2
Now you can work with default metadata provider, it will require entity source files only if you do not provide entity or type options in your decorators (you can use entity callback to use reference to entity class instead of using string name in type, handle for refactoring via IDE like webstorm).
Original answer:
You should ship the typescript code too, and let the cache regenerate on the server - cache would be rebuilt anyway as it checks absolute path to cached entity for invalidation.
You could implement your own cache adapter or metadata provider to get around this, if you don't want to ship the typescript code.
This is how you could implement custom metadata provider that simply throws error when the type option is missing:
import { MetadataProvider, Utils } from 'mikro-orm';
import { EntityMetadata } from 'mikro-orm/dist/decorators';
export class SimpleMetadataProvider extends MetadataProvider {
async loadEntityMetadata(meta: EntityMetadata, name: string): Promise<void> {
// init types and column names
Object.values(meta.properties).forEach(prop => {
if (prop.entity) {
prop.type = Utils.className(prop.entity());
} else if (!prop.type) {
throw new Error(`type is missing for ${meta.name}.${prop.name}`)
}
});
}
}
Then provide this class when initializing:
const orm = await MikroORM.init({
// ...
metadataProvider: SimpleMetadataProvider,
});
The value of type should be JS types, like string/number/Date... You can observe your cached metadata to be sure what values should be there.
Also keep in mind that without TS metadata provider, you will need to specify entity type in #ManyToOne decorator too (either via entity callback, or as a string via type).

Getting a list of tagged services in my controller

What i want is to add services to the service container that i want to use later in my controller or service.
So i created two services with my custom tag fbeen.admin
here they are:
services:
app.test:
class: AppBundle\Admin\TestAdmin
tags:
- { name: fbeen.admin }
fbeen.admin.test:
class: Fbeen\AdminBundle\Admin\TestAdmin
tags:
- { name: fbeen.admin }
Now i want to use all the services with the tag fbeen.admin in my controller but i dont know how.
I followed the How to work with service tags tutorial but i get stuck on this rule:
$definition->addMethodCall('addTransport', array(new Reference($id)));
On some way the addTransport method of the TransportChain class should be called but it seems that it isn't been called.
And even if it would be called then i still do not have a list of services with the fbeen.admin tag into my controller.
I am sure that i am missing something but who can explain me what it is?
p.s. I know that compilerPass runs at buildtime but for example sonata admin knows all admin classes and twig knows all twig extensions. How do they know?
Thank you for reading this :-)
Symfony 3.3
Container gets compiled once (in debug more often, but in production only once). What you manage with addMethodCall... is that once you request your service from container, which you are storing in $definition (that in this case is controller). Then container will call method addMethodCall('method'.. during initialising your service.
What it will look in container:
// This is pseudo content of compiled container
$service = new MyController();
// This is what compiler pass addMethodCall will add, now its your
// responsibility to implement method addAdmin to store admins in for
// example class variable. This is as well way which sonata is using
$service->addAdmin(new AppBundle\Admin\TestAdmin());
$service->addAdmin(new AppBundle\Admin\TestAdmin());
return $service; // So you get fully initialized service
Symfony 3.4+
What you can do is this:
// Your services.yaml
services:
App/MyController/WantToInjectSerivcesController:
arguments:
$admins: !tagged fbeen.admin
// Your controller
class WantToInjectSerivcesController {
public function __construct(iterable $admins) {
foreach ($admins as $admin) {
// you hot your services here
}
}
}
Bonus autotagging of your services. Lets say all your controllers implements interface AdminInterface.
// In your extension where you building container or your kernel build method
$container->registerForAutoconfiguration(AdminInterface::class)->addTag('fbeen.admin');
This will tag automatically all services which implement your interface with tag. So you don't need to set tag explicitly.
The thing to note here is this: The CompilerPass doesn't run the 'addTransport' (or whatever you may call it) in the compiler-pass itself - just says 'when the time is right - run $definition->addTransport(...) class, with this data'. The place to look for where that happens is in your cache directory (grep -R TransportChain var/cache/), where it sets up the $transportChain->addTransport(...).
When you come to use that service for the first time - only then is the data filled in as the class is being instantiated from the container.
This worked for me:
extend the TransportChain class with a getTransports method:
public function getTransports()
{
return $this->transports;
}
and use the TransportChain service in my controller:
use AppBundle\Mail\TransportChain;
$transportChain = $this->get(TransportChain::class);
$transports = $transportChain->getTransports();
// $transports is now an array with all the tagged services
Thank you Alister Bulman for pushing me forwards :-)

symfony 3.1 Check if a bundle is installed

I'm developing a bundle who has a dependency on another one.
In order to handle the case that the base bundle has not been installed I'll like to perform a "bundle_exists()" function inside a controller.
The question is: How can I have a list of installed bundles or How can I check for the name (eventually also the version) of a bundle.
Thanks.
In addition to #Rooneyl's answer:
The best place to do such a check is inside your DI extension (e.g. AcmeDemoExtension). This is executed once the container is build and dumped to cache. There is no need to check such thing on each request (the container doesn't change while it's cached anyway), it'll only slow down your cache.
// ...
class AcmeDemoExtension extends Extension
{
public function load(array $configs, ContainerBuilder $container)
{
$bundles = $container->getParameter('bundles');
if (!isset($bundles['YourDependentBundle'])) {
throw new \InvalidArgumentException(
'The bundle ... needs to be registered in order to use AcmeDemoBundle.'
);
}
}
}
Your class needs to have access to the container object (either by extending or DI).
Then you can do;
$this->container->getParameter('kernel.bundles');
This will give you a list of bundles installed.
Update;
If you are in a controller that extends the Symfony\Bundle\FrameworkBundle\Controller\Controller or in a command class that extends Symfony\Bundle\FrameworkBundle\Command\ContainerAwareCommand, you can just get the parameter.
$this->getParameter('kernel.bundles').
Else #Wouter J's answer is your best answer.
You can get a list of all Bundles from the Kernel like this:
public function indexAction ()
{
$arrBundles = $this->get("kernel")->getBundles();
if (!array_key_exists("MyBundle", $arrBundles))
{
// bundle not found
}
}
From Andrey at this question: How do I get a list of bundles in symfony2?
If you want to call a non static method of registered bundle object (not class) then you can do the following:
$kernel = $this->container->get('kernel');
$bundles = $kernel->getBundles();
$bundles['YourBundleName']->someMethod();
Where 'YourBundleName' is the name of your bundle, which you can get by calling from console:
php app/console config:dump-reference

Resources