Symfony SAAS multitenant (DB by tenant). Sharing my implementation - symfony

I people! First: sorry about my english (I'm improving it...). Thanks for edit de post to corret it :)
I'm implementing a SaaS app multitenant on symfony. I wish share with the community what is the way that I've used (mae by not the best, but it works fine...).
THE CONCEPT:
One code (one app).
A DB for each tenant
A CSS for each tenant
Each client have a subdomain like (www.tenant1.myapp.com, www.tenant2.myapp.com)
FIRST: ARRANGING THE CONFIGURATION
I've arranged the configuration files in folders like this:
-app
---config
-----tenantA
--------config.yml
--------config_dev.yml
--------config_prod.yml
--------parameters.yml
-----tenantB
--------config.yml
--------config_dev.yml
--------config_prod.yml
--------parameters.yml
-----services.yml
-----security.yml
DISPATCHER
On AppKernel I've modiffied the registerContainerConfiguration action. It loads de general configuration (DB parameters, twig globals, etc....).
Original:
public function registerContainerConfiguration(LoaderInterface $loader)
{
$loader->load($this->getRootDir().'/config/config_'.$this->getEnvironment().'.yml');
}
New (I used the subdomain to set what is the environment that symfony have to load:
public function registerContainerConfiguration(LoaderInterface $loader)
{
$url= $_SERVER['HTTP_HOST'];
$partes = explode('.',$url);
$tenant= $partes[0]; // this get de subdomain. If the access is www.tenant1.myapp.com this returns tenant1.
$loader->load($this->getRootDir().'/config/'.$tenant.'/config_'.$this->getEnvironment().'.yml');
}
This function goes to the tenant folder and load the config file with his parameters.yml
ARRANGING CACHE AND LOG FOLDERS
I've modified the AppKernel including the functions getCacheDir and getLogDir in order to set the correct folder to save the files:
public function getCacheDir()
{
return $this->rootDir . '/cache/' . $tenant. '/' . $this->environment;
}
public function getLogDir()
{
return $this->rootDir . '/logs/' . $tenant. '/' . $this->environment;
}
This save the files like:
-cache
---tenant1
-----dev
-----prod
---tenant2
-----dev
-----prod
-logs
---tenant1
-----dev.log
-----prod.log
----tenant2
------dev.log
------prod.log
CSS BY TENANT
I've arranged my css files by tenant like this:
--web
---css
----tenant1.css
----tenant2.css
----tenantN.css
Then, in the config.yml of each tenant (remind the arranged folders on app->config) I used a global tiwg with the name of the css theme of the tenant:
twig:
globals:
app_css: tenant1.css
Then, in the tiwg where i've declared the css files and include in the base twig I call the css by:
{% set cssload = "css/" ~ app_css ~ ".css" %}
<link rel="stylesheet" href="/{{cssload}}">
As the cache is saved in diferents folders, the CSS of tenant1 don't affects the cache and the css of tenant2.
Finish: I don't know if this is a good practice, but it works fine, and it's another solution.
Thanks everybody for the help of this days!!!!!

Related

upgrade symfony3.4 with bundles extension to symfony4 flex

I have a project in symfony 3.4 then I tried to upgrade to symfony 4 /flex.
the problem is in my code there a 3 bundles TBAdminBundle, TBPlatformBundle and TBSecurityBundle.
Also for each bundle there Extension class exemple.
class TBPlatformExtension extends Extension
{
public function load(array $configs, ContainerBuilder $container)
{
$configuration = new PlatformConfiguration();
$config = $this->processConfiguration($configuration, $configs);
$loader = new Loader\YamlFileLoader($container, new FileLocator(__DIR__.'/../Resources/config'));
$loader->load('services.yml');
if (!in_array(strtolower($config['db_driver']), array('custom', 'mongodb', 'orm'))) {
throw new \InvalidArgumentException(sprintf('Invalid db driver "%s".', $config['db_driver']));
}
if ('custom' !== $config['db_driver']) {
$loader->load(sprintf('%s.yml', $config['db_driver']));
$def = new Definition('Doctrine\ORM\EntityManager', array('%tb_notification.model_manager_name%'));
$def->setPublic(false);
if (method_exists($def, 'setFactory')) {
$def->setFactory(array(new Reference('doctrine'), 'getManager'));
} else {
// To be removed when dependency on Symfony DependencyInjection is bumped to 2.6
$def->setFactoryService('doctrine');
$def->setFactoryMethod('getManager');
}
$container->setDefinition('tb_notification.entity_manager', $def);
}
foreach (array('form', 'command', 'events') as $basename) {
$loader->load(sprintf('%s.yml', $basename));
}
$container->setParameter('tb_notification.model_manager_name', $config['model_manager_name']);
$container->setParameter('tb_notification.form.notification.type', $config['form']['notification']['type']);
$container->setParameter('tb_notification.form.notification.name', $config['form']['notification']['name']);
$container->setParameter('tb_trip.form.trip.type', $config['form']['trip']['type']);
$container->setParameter('tb_trip.form.trip.name', $config['form']['trip']['name']);
$container->setParameter(
'tb_notification.form.delete_notification.type',
$config['form']['delete_notification']['type']
);
$container->setParameter(
'tb_notification.form.delete_notification.name',
$config['form']['delete_notification']['name']
);
$container->setParameter('tb_trip.form.trip.update_trip.type', $config['form']['update_trip']['type']);
$container->setParameter('tb_trip.form.trip.update_trip.name', $config['form']['update_trip']['name']);
$container->setParameter('tb_action.form.action.type', $config['form']['action']['type']);
$container->setParameter('tb_action.form.action.name', $config['form']['action']['name']);
$container->setParameter('tb_place.form.place.type', $config['form']['place']['type']);
$container->setParameter('tb_place.form.place.name', $config['form']['place']['name']);
$container->setParameter('tb_action_privacy.form.action.type', $config['form']['privacy']['type']);
$container->setParameter('tb_action_privacy.form.action.name', $config['form']['privacy']['name']);
$container->setParameter('tb_notification.model.notification.class', $config['class']['model']['notification']);
$container->setParameter('tb_trip.model.trip.class', $config['class']['model']['trip']);
$container->setParameter('tb_like.model.like.class', $config['class']['model']['like']);
$container->setParameter('tb_helpful.model.helpful.class', $config['class']['model']['helpful']);
$container->setParameter('tb_action.model.action.class', $config['class']['model']['action']);
$container->setParameter('tb_budget.model.class', $config['class']['model']['budget']);
$container->setParameter('tb_action.model.action_privacy_policy.class', $config['class']['model']['privacy']);
$container->setParameter('tb_rating.model.rating.class', $config['class']['model']['vote']);
$container->setParameter('tb_place.model.place.class', $config['class']['model']['place']);
$container->setParameter('tb_destination.model.destination.class', $config['class']['model']['destination']);
// parameters for hydrating object with doctrine
$container->setParameter('tb_action.hydrate.action', $config['hydrate']['action']);
$container->setParameter('tb_google.key', $config['google']['key']);
$container->setParameter('compare_text.default_percent', $config['compare']['text']);
$container->setAlias('tb_trip.manager.trip', $config['service']['manager']['trip']);
$container->setAlias('tb_like.manager.post', $config['service']['manager']['like']);
$container->setAlias('tb_helpful.manager.post', $config['service']['manager']['helpful']);
$container->setAlias('tb_budget_manager', $config['service']['manager']['budget']);
$container->setAlias('tb_place_manager', $config['service']['manager']['place']);
$container->setAlias('tb_destination_manager', $config['service']['manager']['destination']);
$container->setAlias('tb_compare_string', 'similar_text.manager');
$container->setAlias('foursquare.manager', 'foursquare.manager.default');
$container->setAlias('tb_action.form_factory', $config['service']['form_factory']['action']);
$container->setAlias('tb_place.form_factory', $config['service']['form_factory']['place']);
$container->setAlias('tb_action_privacy.form_factory', $config['service']['form_factory']['privacy']);
$container->setAlias(
'tb_notification.form_factory.notification',
$config['service']['form_factory']['notification']
);
$container->setAlias('tb_trip.form_factory', $config['service']['form_factory']['trip']);
$container->setAlias(
'tb_notification.form_factory.notification_delete',
$config['service']['form_factory']['delete_notification']
);
$container->setAlias('tb_action.customer_repository', 'tb_action.customer_repository_default');
$container->setAlias('tb_notification.customer_repository', 'tb_notification.customer_repository_default');
$container->setAlias('tb_trip.customer_repository', 'tb_trip.customer_repository_default');
}
}
According to documentation
"In Symfony versions prior to 4.0, it was recommended to organize your own application code using bundles. This is no longer recommended and bundles should only be used to share code and features between multiple applications."
I know this it's no more possible with symfony4/flex
How can I rewrite this to match with flex configuration?
thanks
My suggestion is to migrate progressively your bundles.
1. Put the bundle in the src of your SF4 application like this :
src/
Bundle/
TBAdminBundle
TBPlatformBundle
TBSecurityBundle
Command
Controller
Entity
...
2. Use Composer PSR-4 to autoload them (composer.json)
"autoload" : {
"psr-4" : {
"App\\" : "src/",
"TBAdminBundle\\" : "src/Bundle/TBAdminBundle/",
"TBPlatformBundle\\" : "src/Bundle/TBPlatformBundle/",
"TBSecurityBundle\\" : "src/Bundle/TBSecurityBundle/",
...
3. Exclude them from App Services autoload (config/services.yaml)
App\:
resource: '../src/*'
# you can exclude directories or files
# but if a service is unused, it's removed anyway
exclude: '../src/{Entity,Migrations,Repository,Bundle}'
4. Validate the new installation/configuration
Although SF4 is bundle less for core App application, it supports bundle ... Just check that you resolve all deprecated functions from those bundles
5. Start progressive migration of your bundles code:
As Hint :
Rework your config/parameters to use ENV, and services.yaml parameters
So you can transfer most config variables/parameters to the App level and easily share them ...
All your services definitions can be moved from Extention classes into services.yaml for simplicity and easy maintenance.
You will degrease bundles code as time go and you gain experience with the new SF4 services usability and orientations.
That note tells you that it is no longer a recommendation but it doesn't mean that you should refactor your code completely. If you already have your code packed as bundles you should only remove deprecated code. In general it is a good idea to pack your code in bundles only if it is reusable across projects.

Symfony call a reusable bundle like a service

I'm trying to create a reusable bundle to manage basket in webstore application...
I create every desired action in a reusable bundle using a controller like addBasketAction, removeBasketAction and so on... The classical
Everythings works fine, but now I'm litteraly stuck about how to "call" my bundle from my "principal" application
You dont call a bundle but you have to register your bundle in the AppKernel.php file:
class AppKernel extends Kernel
{
public function registerBundles()
{
$bundles = [
// ...
new AppBundle\AppBundle(),
new Company\CustomBundle\CompanyCustomBundle(),
];
}
// ...
}
Then you will need to import your routes too but i dont know if you use annotations for your routes or a yaml file.
read more:
http://symfony.com/doc/current/bundles.html
http://symfony.com/doc/current/bundles/best_practices.html
[edit]
Reading your topic title it looks like that you are confusing bundles and services. You can add a service in your bundle on the same way as you can add this service in your AppBundle:
http://symfony.com/doc/current/service_container.html#creating-configuring-services-in-the-container

Custom route configuration with Silex

I know that the basis of Silex approach in which all the application logic in a single file. But my application will be possible to have more than twenty controllers. So I want to have a handy map to manage the router.
My question is to search for solutions in which I would be able to make a router to a separate file. In the best case, the file must be of YAML type:
# config/routing.yml
_home:
pattern: /
defaults: { _controller: MyProject\Controller\MyController::index }
But the native is also a good case (for me):
$routes = new RouteCollection();
$routes->add(
'home',
new Route('/', array('controller' => 'MyProject\Controller\MyController::index')
));
return $routes;
Problem of the second case is that I have to use the match() function for each rule of routing. It is not at all clear.
What are the ways to solve this issue? The condition is that I want to use the existing API Silex or components of Symfony2.
Small note:
I don't use a ControllerProviderInterface for my Controller classes. This is an independent classes.
First of all, the basis of Silex is not that you put everything in one file. The basis of Silex is that you create your own 'framework', your own way of organizing applications.
"Use silex if you are comfortable with making all of your own architecture decisions and full stack Symfony2 if not."
-- Dustin Whittle
Read more about this in this blogpost, created by the creator of Silex.
How to solve your problem
What you basically want is to parse a Yaml file and get the pattern and defaults._controller settings from each route that is parsed.
To parse a Yaml file, you can use the Yaml Component of Symfony2. You get an array back which you can use to add the route to Silex:
// parse the yaml file
$routes = ...;
$app = new Silex\Application();
foreach ($routes as $route) {
$app->match($route['pattern'], $route['defaults']['_controller']);
}
// ...
$app->run();
I thought I'd add my method here as, although others may work, there isn't really a simple solution. Adding FileLocator / YamlFileLoader adds a load of bulk that I don't want in my application just to read / parse a yaml file.
Composer
First, you're going to need to include the relevant files. The symfony YAML component, and a really simple and useful config service provider by someone who actively works on Silex.
"require": {
"symfony/yaml": "~2.3",
"igorw/config-service-provider": "1.2.*"
}
File
Let's say that your routes file looks like this (routes.yml):
config.routes:
dashboard:
pattern: /
defaults: { _controller: 'IndexController::indexAction' }
method: GET
Registration
Individually register each yaml file. The first key in the file is the name it will be available under your $app variable (handled by the pimple service locator).
$this->register(new ConfigServiceProvider(__DIR__."/../config/services.yml"));
$this->register(new ConfigServiceProvider(__DIR__."/../config/routes.yml"));
// any more yaml files you like
Routes
You can get these routes using the following:
$routes = $app['config.routes']; // See the first key in the yaml file for this name
foreach ($routes as $name => $route)
{
$app->match($route['pattern'], $route['defaults']['_controller'])->bind($name)->method(isset($route['method'])?$route['method']:'GET');
}
->bind() allows you to 'name' your urls to be used within twig, for example.
->method() allows you to specify POST | GET. You'll note that I defaulted it to 'GET' with a ternary there if the route doesn't specify a method.
Ok, that's how I solved it.
This method is part of my application and called before run():
# /src/Application.php
...
protected function _initRoutes()
{
$locator = new FileLocator(__DIR__.'/config');
$loader = new YamlFileLoader($locator);
$this['routes'] = $loader->load('routes.yml');
}
Application class is my own and it extends Silex\Application.
Configuration file:
# /src/config/routes.yml
home:
pattern: /
defaults: { _controller: '\MyDemoSite\Controllers\DefaultController::indexAction' }
It works fine for me!
UPD:
I think this is the right option to add collections:
$this['routes']->addCollection($loader->load('routes.yml'));
More flexible.
You could extend the routes service (which is a RouteCollection), and load a YAML file with FileLocator and YamlFileLoader:
use Symfony\Component\Config\FileLocator;
use Symfony\Component\Routing\Loader\YamlFileLoader;
$app->extend('routes', function($routeCollection) {
$locator = new FileLocator([__DIR__ . '/../config']);
$loader = new YamlFileLoader($locator);
$collection = $loader->load('routes.yml');
$routeCollection->addCollection($collection);
return $routeCollection;
});
You will need symfony/config and symfony/yaml dependencies though.

Can I include an optional config file in Symfony2?

I want to make a local config file, config_local.yml, that allows each development environment to be configured correctly without screwing up other people's dev environments. I want it to be a separate file so that I can "gitignore" it and know that nothing essential is missing from the project, while simultaneously not having the issue of git constantly telling me that config_dev.yml has new changes (and running the risk of someone committing those changes).
Right now, I have config_dev.yml doing
imports:
- { resource: config_local.yml }
which is great, unless the file doesn't exist (i.e. for a new clone of the repository).
My question is: Is there any way to make this include optional? I.e., If the file exists then import it, otherwise ignore it.
Edit: I was hoping for a syntax like:
imports:
- { resource: config.yml }
? { resource: config_local.yml }
I know this is a really old question, and I do think the approved solution is better I thought I would give a simpler solution which has the benefit of not changing any code
You can use the ignore_errors option, which won't display any errors if the file doesn't exist
imports:
- { resource: config_local.yml, ignore_errors: true }
Warning, if you DO have a syntax error in the file, it will also be ignored, so if you have unexpected results, check to make sure there is no syntax error or other error in the file.
There is another option.
on app/appKernel.php change the registerContainerConfiguration method to this :
public function registerContainerConfiguration(LoaderInterface $loader)
{
$loader->load(__DIR__.'/config/config_'.$this->getEnvironment().'.yml');
$extrafiles = array (
__DIR__.'/config/config_local.yml',
);
foreach ($extrafiles as $filename) {
if (file_exists($filename) && is_readable($filename)) {
$loader->load($filename);
}
}
}
this way you have a global config_local.yml file that overwrites the config_env.yml files
A solution is to create a separate environment, which is explained in the Symfony2 cookbook. If you do not wish to create one, there is another way involving the creation of an extension.
// src/Acme/Bundle/AcmeDemo/DepencendyInjection/AcmeDemoExtension.php
namespace Acme\DemoBundle\DependencyInjection;
use Symfony\Component\Config\FileLocator;
use Symfony\Component\DependencyInjection\ContainerBuilder;
use Symfony\Component\DependencyInjection\Loader\YamlFileLoader;
use Symfony\Component\HttpKernel\DependencyInjection\Extension;
class AcmeDemoExtension extends Extension
{
public function load(array $configs, ContainerBuilder $container)
{
// All following files will be loaded from the configuration directory
// of your bundle. You may change the location to /app/ of course.
$loader = new YamlFileLoader($container, new FileLocator(__DIR__.'/../Resources/config'));
try
{
$loader->load('config_local.yml');
}
catch(\InvalidArgumentException $e)
{
// File was not found
}
}
}
Some digging in the Symfony code revealed me that YamlFileLoader::load() FileLocator::locate() will throw \InvalidArgumentException, if a file is not found. It is invoked by YamlFileLoader::load().
If you use the naming conventions, the extension will be automatically executed. For a more thorough explanation, visit this blog.
I tried both above answers but none did work for me.
i made a new environment: "local" that imports "dev", but as you can read here: There is no extension able to load the configuration for "web_profiler" you also had to hack the AppKernel class.
Further you couldnt set config_local.yml to .gitignore because the file is necessary in local env.
Since i had to hack the AppKernel anyway i tried the approach with the $extrafiles but that resulted in "ForbiddenOverwriteException"
So now what worked for me was a modification of the $extrafiles approach:
replace in app/AppKernel.php
$loader->load(__DIR__ . '/config/config_' . $this->getEnvironment() . '.yml');
with
if ($this->getEnvironment() == 'dev') {
$extrafiles = array(
__DIR__ . '/config/config_local.yml',
);
foreach ($extrafiles as $filename) {
if (file_exists($filename) && is_readable($filename)) {
$loader->load($filename);
}
}
} else {
$loader->load(__DIR__ . '/config/config_' . $this->getEnvironment() . '.yml');
}

PHPUnit inclusion path issues

This one's got me stumped. I've been working with PHPUnit for a couple of months now, so I'm not that green...but I look forward to being pointed in the direction of the obvious mistake I'm making! The initialisation process outlined below works fine if I run the "app" from a browser - but PHPUnit is choking...can any one put me out of my misery?
I'm trying to test a homebrew MVC, for study purposes. It follows a typical ZF layout.
Here's the index page:
include './../library/SKL/Application.php';
$SKL_Application = new SKL_Application();
$SKL_Application->initialise('./../application/configs/config.ini');
Here's the application class (early days...)
include 'bootstrap.php';
class SKL_Application {
/**
* initialises the application
*/
public function initialise($file) {
$this->processBootstrap();
//purely to test PHPUnit is working as expected
return true;
}
/**
* iterates over bootstrap class and executes
* all methods prefixed with "_init"
*/
private function processBootstrap() {
$Bootstrap = new Bootstrap();
$bootstrap_methods = get_class_methods($Bootstrap);
foreach ($bootstrap_methods as $method) {
if(substr($method,0,5) == '_init'){
$bootstrap->$method();
}
}
return true;
}
}
Here's the test:
require_once dirname(__FILE__).'/../../../public/bootstrap.php';
require_once dirname(__FILE__).'/../../../library/SKL/Application.php';
class SKL_ApplicationTest extends PHPUnit_Framework_TestCase {
protected $object;
protected function setUp() {
$this->object = new SKL_Application();
}
/**
* Tears down the fixture, for example, closes a network connection.
* This method is called after a test is executed.
*/
protected function tearDown() {
}
public function testInitialise() {
$this->assertType('boolean',$this->object->initialise());
}
}
But I keep stumbling at the first hurdle!!
PHP Warning: include(bootstrap.php): failed to open stream:
No such file or directory in path\to\files\SKL\Application.php on line 9
any ideas?
Use include_once or better yet require_once instead of include to include the bootstrap.php in the Application class file. Despite being already loaded include loads it again but since it's obviously not on the include path you get the error.
Thanks to Raoul Duke for giving me a push in the right direction, here's where I got to so far
1 - add the root of the application to the include path
2 - make all inclusion paths relative to the root of the application
3 - include a file in your unit tests that performs the same function, but compensates for the relative location when it is included. I just used realpath() on the directory location of the files.
The problem I have now is that the darn thing won't see any additional files I'm trying to pass it.
So, I'm trying to test a configuration class, that will parse a variety of filetypes dynamically. The directory structure is like this:
Application_ConfigTest.php
config.ini
The first test:
public function testParseFile() {
$this->assertType('array',$this->object->parseFile('config.ini'));
}
The error:
failed to open stream: No such file or directory
WTF? It's IN the same directory as the test class...
I solved this by providing an absolute (i.e. file structure) path to the configuration file.Can anyone explain to me how PHPUnit resolves it's paths, or is it because the test class itself is included elsewhere, rendering relative paths meaningless?

Resources