I am familiar with PHP, but only just learning Symfony2 and Doctrine. I am wondering what the best practice is for static data, as in data that is only ever updated when deploying a new version of the web application to production.
I would prefer to specify static data (not schemas) in YAML, because then modifying that data is easy for everyone, whether they know any PHP/Doctrine or not. I would like for non-developers to be able to add an achievement by modifying the .yml file. An example of a static YAML database that I would like to maintain is:
Achievements:
Conservative:
Difficulty: 2
Description: >
Description of Conservative Achievement.
Dedicated:
Difficulty: 3
Description: >
Description of Dedicated Achievement.
Persistent:
Difficulty: 2
Description: >
Description of Persistent Achievement.
Now imagine I have an entity representing a User
// src/Paulpro/ExperimentingBundle/Entity/User.php
namespace Paulpro\ExperimentingBundle\Entity;
use Doctrine\Common\Collections\ArrayCollection;
class User {
protected $name;
protected $achievements;
public function __construct(){
// Collection of achievements as defined by achievements.yml
// $this->achievements = new ArrayCollection();
}
}
I want to use Doctrine as normal for Users, so that they are stored in the database and I want Users to be able to earn achievements. A user can have multiple of each achievement, so in my User entity I need some way to represent a collection of achievements with quantities. I do not want the achievements difficulties and descriptions to be stored in the database, only in the .yml file, unless there is a good reason to store the achievements themselves in the database and a good way to import the static data into the database as part of automatic deployment.
I have three main questions related to this problem:
Is there a better way to do this, keeping in mind that I want non-developers to be able to add achievements easily and I will probably want to overwrite the achievements.yml file for different locales?
Where in my Symfony2 bundle should I put the achievements.yml file(s)?
How should I modify the User entity so that the resulting database can maintain quantities of achievements per user?
I do not want the achievements difficulties and descriptions to be
stored in the database, only in the .yml file, unless there is a good
reason to store the achievements themselves in the database and a good
way to import the static data into the database as part of automatic
deployment.
A good reason: It will be easier to manage the relations between Users and Achievements.
A way to import the static data into the database: DoctrineFixturesBundle
1. Define your static configuration
The best way to do so, is to expose a semantic configuration.
In your case, you will have the following 2 files:
// src/Paulpro/ExperimentingBundle/DependencyExtension/Configuration.php
<?php
namespace Paulpro\ExperimentingBundle\DependencyInjection;
use Symfony\Component\Config\Definition\Builder\TreeBuilder;
use Symfony\Component\Config\Definition\ConfigurationInterface;
class Configuration implements ConfigurationInterface
{
/**
* Defines the configuration tree for the bundle
*
* #return \Symfony\Component\Config\Definition\Builder\TreeBuilder
*/
public function getConfigTreeBuilder()
{
$treeBuilder = new TreeBuilder();
$rootNode = $treeBuilder->root('paulpro_experimenting');
$rootNode
->children()
->arrayNode('Achievements')->addDefaultsIfNotSet()
->children()
->arrayNode('Conservative')->addDefaultsIfNotSet()
->children()
->integerNode('Difficulty')->defaultValue(2)->end()
->scalarNode('Description')->defaultValue('Description of Conservative Achievement.')->end()
->end()
->end()
->arrayNode('Dedicated')->addDefaultsIfNotSet()
->children()
->integerNode('Difficulty')->defaultValue(3)->end()
->scalarNode('Description')->defaultValue('Description of Dedicated Achievement.')->end()
->end()
->end()
->arrayNode('Persistent')->addDefaultsIfNotSet()
->children()
->integerNode('Difficulty')->defaultValue(2)->end()
->scalarNode('Description')->defaultValue('Description of Persistent Achievement.')->end()
->end()
->end();
return $treeBuilder;
}
}
and:
<?php
namespace Paulpro\ExperimentingBundle\DependencyInjection;
use Symfony\Component\DependencyInjection\ContainerBuilder;
use Symfony\Component\HttpKernel\DependencyInjection\Extension;
class PaulproExperimentingExtension extends Extension
{
/**
* Load the configuration for the bundle
*
* #param array $configs
* #param \Symfony\Component\DependencyInjection\ContainerBuilder $container
*/
public function load(array $configs, ContainerBuilder $container)
{
$configuration = new Configuration();
$config = $this->processConfiguration($configuration, $configs);
foreach($config as $key => $value)
{
$container->setParameter('paulpro_experimenting.'.$key, $value);
}
}
}
Doing so, you will be able to have a better way to manage how your users are using the config. To see a sample of the default config result, you can use the command:
php app/console config:dump-reference PaulProExperimentingBundle
That should result as following:
Default configuration for "PaulProExperimentingBundle"
paulpro_experimenting:
Achievements:
Conservative:
Difficulty: 2
Description: Description of Conservative Achievement.
Dedicated:
Difficulty: 3
Description: Description of Dedicated Achievement.
Persistent:
Difficulty: 2
Description: Description of Persistent Achievement.
This means your users can put this sample in the config.yml under the app\config folder and change it depending on their need. The only condition is that whatever information they put in this file has to be validate by the Configuration tree you have defined.
2. Define your entities
Define the Achievement entity as it best fits your need. You could for example use:
// src/Paulpro/ExperimentingBundle/Entity/Achievement.php
namespace Paulpro\ExperimentingBundle\Entity;
use Doctrine\Common\Collections\ArrayCollection;
class Achievement{
protected $name;
protected $difficulty;
protected $description;
/**
* #ManyToMany(targetEntity="User", mappedBy="achievements")
* #JoinTable(name="users_achievements")
**/
private $users;
public function __construct() {
$this->users = new ArrayCollection();
}
}
You will keep your User entity the way it is except you have to add the relation to Achievement:
// src/Paulpro/ExperimentingBundle/Entity/User.php
namespace Paulpro\ExperimentingBundle\Entity;
use Doctrine\Common\Collections\ArrayCollection;
class User {
protected $name;
/**
* #ManyToMany(targetEntity="Achivement", mappedBy="users")
**/
protected $achievements;
public function __construct(){
$this->achievements = new ArrayCollection();
}
}
3. Fill up the database when deploying the app
This is the last step and it uses exclusively the DoctrineFixturesBundle.
You will have to create the fixture for your Achivement entity:
// src/Paulpro/ExperimentingBundle/DataFixtures/ORM/LoadAchivementData.php
<?php
namespace Paulpro\ExperimentingBundle\DataFixtures\ORM;
use Doctrine\Common\DataFixtures\FixtureInterface;
use Doctrine\Common\Persistence\ObjectManager;
use Paulpro\ExperimentingBundle\Entity\Achivement;
use Symfony\Component\DependencyInjection\ContainerAwareInterface;
use Symfony\Component\DependencyInjection\ContainerInterface;
class LoadTypesData implements FixtureInterface, ContainerAwareInterface
{
private $container;
public function setContainer(ContainerInterface $container = null)
{
$this->container = $container;
}
public function load(ObjectManager $manager)
{
foreach($this->container->getParameter('paulpro_experimenting.Achievements') as $key => $value)
{
$achivement = new Achivement();
$achivement->setName($key);
$achivement->setDifficulty($value['Difficulty']);
$achivement->setDescription($value['Description']);
$manager->persist($achivement);
}
$manager->flush();
}
}
This fixture will go through the config for paulpro_experimenting.Achievements and load the defined Achievements from here.
Lastly, to load the data in the database, you will have to run the following command:
php app/console doctrine:fixtures:load
Et voila, you should now be able to add/remove achievements from your users.
First things first,
If you are concerned about what the achievement thing say or the language used, you can use the translator component. Basically what it does is to store your strings in translation files (YAML). So, where you want to add some string, say the achievement name you add some key achievement.name and the translator looks for the string for that key. You can have multiple translations and Symfony automatically picks the right one based on the locale of the client. The translator is built into the templates so there is nothing to add but you can use it as well anywhere on the framework.
If you want to get data from files (maybe you want not only the translation but also some structured data), there are some things you can do, you don't actually need Doctrine for this.
1) Using config.ini
On every Symfony2 project there is a fille called config.ini (on newer versions of the framework it changed to config.yaml) in the app/config/ directory. That file is where things like database configuration values are meant to be stored BUT you can add as many options as you want, just adding them to the file:
[parameters]
database_driver="pdo_mysql"
database_host="192.168.1.1"
...
secret="..."
my_param= "my value"
Getting those values is easy, from a controller just call:
$this->container->getParameter('my_param'); // returns "my value"
It might work for you, but depending on what kind of data you want to store (for ini files there is just key/value way to go, and for the newer versions using yaml files, I'm just not sure if the framework would parse a hierarchical structure for you (it might, give it a try)).
Other thing to keep in mind is that if someone deletes a configuration value your system will come down (probably after the cache is manually cleared) and there is sensitive data (a.k.a. passwords) that we generally don't want everybody to know!
2) Using YAML component of Symfony2
First create your YAML file in whatever way it makes sense to you, then place it somewhere on your server (maybe in "resources" or "config" dir?)
Then, if you are on a controller, just write something like:
//This line goes at the begining of your file where all the *use*s are
use Symfony\Component\Yaml\Yaml;
$file = __DIR__.'/../Path/To/file.yml'; //case sensitive on Unix-like systems
if(!file_exists($file)){
throw new HttpException(404, "File not found.");
}
//This line does the magic
$index = Yaml::parse(file_get_contents($file));
//Now $index contain an associative array containing all your data.
And you're done! I've used this to load things like menus or generating new sections on a website without touching the code or the database, let your imagination fly!
You can create a console command that import data (from any source you can imagine) to your database and then use that command in some script as part of your deployment process, it's up to you decide if it's worth the effort.
Finally let's just talk about the entity relations thing.
It's not all clear what you are trying to do, but there is no (clean/nice) way to add relationships between a Doctrine entity and a random configuration file. The problem is that they are meant to perform different roles in the app.
I'd suggest you do something like this:
On your database:
Add a table "achievements" with an id and a column called nameKey and maybe descriptionKey ... These __Key columns will contain translator keys (as mentioned before). I.e:
"ID":1, "nameKey":"achievement.1.name", "descriptionKey":"achievement.1.description"
"ID":2, "nameKey":"achievement.2.name", "descriptionKey":"achievement.2.description"
"ID":3, "nameKey":"achievement.3.name", "descriptionKey":"achievement.3.description"
Then modify your User so it links to the achievements table.
Keeping this in your database will help you and enable you to use some other nice Symfony tools (like forms, validators, ...) and help you keep integrity in your data.
Using translator
Create a translation file for each lang. you are interested, they will look like:
achievement.1.name: "Best player ever"
achievement.1.description: "Blah blah blah"
achievement.2.name: "Better than average"
achievement.2.description: "Blah blah blah"
just follow the documentation to see how to output your strings translated wherever you want to put them.
To add new records you would need to add a record on the database (if you want to make it easier, just write a small web form for that) and the translation keys.
Symfony is so powerful yet easy that will make you lazy and productive at the same time
I would propose to use a class to represent your Achievements.
I would also propose to use a relational database to store relations between Users and Achievements (logic :) )
I mean that I would prefer to store achievement in database.
Using alice it's quite easy, and your yaml file has not that much to change.
It's a 2 steps process:
modifiy yaml to fit alice needs
create an Achievement entity and add relations between user and it
Achievement:
Conservative:
id: 1
Difficulty: 2
Description: >
Description of Conservative Achievement.
<?php
class Achievement
{
public $id;
public $difficulty;
public $description;
}
Follow then the alice steps, and you have a working solution.
Another possibility is to simply store the Achievement keys as serialized array in your user entity:
<?php
class User
{
/** #ORM\Column(type="array") **/
protected $achievements = array();
public function addAchievementByName($name)
{
$this->achievements[] = $name;
}
function getAchievements()
{
$self = $this;
$allAchievements = Yaml::load(__DIR__.'/path/yo/yaml/file');
return array_flip(array_filter(array_flip($allAchievements), function($achievement) use ($self) {
return in_array($achievement, $self->achievements);
}));
}
}
Related
Is it considered a bad practice to add fields to Symfony entity in controller? For example lets say that I have a simple entity:
/**
* #ORM\Entity
* #ORM\Table(name="user")
*/
class User extends BaseUser
{
/**
* #ORM\Id
* #ORM\Column(type="integer")
* #ORM\GeneratedValue(strategy="AUTO")
*/
protected $id;
public function __construct()
{
parent::__construct();
}
public function getId()
{
return $this->id;
}
public function setId($id)
{
$this->id = $id;
}
}
And then in UserController.php I want to do the following:
foreach($users as $user){
$user->postsCount = someMethodThatWillCountPosts();
}
So later that postsCount can be displayed in Twig. Is it a bad practice?
Edit:
It's important to count posts on side of mysql database, there will be more than 50.000 elements to count for each user.
Edit2:
Please take a note that this questions is not about some particular problem but rather about good and bad practices in object oriented programming in Symfony.
As #Rooneyl explained that if you have relation between user and post then you can get count easily in your controller, refer this for the same. But if you are looking to constructing and using more complex queries from inside a controller. In order to isolate, reuse and test these queries, it's a good practice to create a custom repository class for your entity.Methods containing your query logic can then be stored in this class.
To do this, add the repository class name to your entity's mapping definition:
// src/AppBundle/Entity/Product.php
namespace AppBundle\Entity;
use Doctrine\ORM\Mapping as ORM;
/**
* #ORM\Entity(repositoryClass="AppBundle\Repository\ProductRepository")
*/
class Product
{
//...
}
Doctrine can generate empty repository classes for all the entities in your application via the same command used earlier to generate the missing getter and setter methods:
$ php bin/console doctrine:generate:entities AppBundle
If you opt to create the repository classes yourself, they must extend
Doctrine\ORM\EntityRepository.
More Deatils
Updated Answer
In many cases associations between entities can get pretty large. Even in a simple scenario like a blog. where posts can be commented, you always have to assume that a post draws hundreds of comments. In Doctrine 2.0 if you accessed an association it would always get loaded completely into memory. This can lead to pretty serious performance problems, if your associations contain several hundreds or thousands of entities.
With Doctrine 2.1 a feature called Extra Lazy is introduced for associations. Associations are marked as Lazy by default, which means the whole collection object for an association is populated the first time its accessed. If you mark an association as extra lazy the following methods on collections can be called without triggering a full load of the collection: SOURCE
"rather about good and bad practices in object oriented programming"
If that's the case then you really shouldn't have any business logic in controller, you should move this to services.
So if you need to do something with entities before passing them to twig template you might want to do that in specific service or have a custom repository class that does that (maybe using some other service class) before returning the results.
i.e. then your controller's action could look more like that:
public function someAction()
{
//using custom repository
$users = $this->usersRepo->getWithPostCount()
//or using some other service
//$users = $this->usersFormatter->getWithPostCount(x)
return $this->render('SomeBundle:Default:index.html.twig', [
users => $users
]);
}
It's really up to you how you're going to do it, the main point to take here is that best practices rather discourage from having any biz logic in controller. Just imagine you'll need to do the same thing in another controller, or yet some other service. If you don't encapsulate it in it's own service then you'll need to write it every single time.
btw. have a read there:
http://symfony.com/doc/current/best_practices/index.html
let say that I have something like this:
class User{
/**
* Object that is lazy loaded
*/
private $statistic; //object with some stored data and some calculated data
}
Some of the $statistic's properties are stored in the DB but some other of them are calculated by analyzing the user activity (querying data records).
the thing is that I got a $user and when I run $user->getStatistic() as spected, I get the stored $statistic data and I need to add more data using sql queries and I don't know where to program this functionality.
¿overriding the Repository? I try overriding the find() method but it doesn't work
I know that if I use the active record pattern this can be done with no problem giving that I can access the DB in the construct method or the getters maybe, etc.
but I don't know how this could be done with doctrine standard behavior.
I believe that there must be a way to ensure that every instance of the Statistic Class have this calculated data on it.
I'm using symfony... maybe a service or something...
There are a number of ways to solve your problem.
Doctrine listener
This is probably the easiest one. Use Doctrine postLoad event to fill out data you need on your User model.
This is a completely valid approach, but has a couple of drawbacks:
This will be ran every time doctrine fetches an User entity instance from database. If it fetches a list of 100 users, it will be ran 100 times. This could make a performance problem if you do some time-consuming tasks in there.
They are hard to debug: if an error is encountered, events usually make code flow a lot less clear and therefore make debugging harder. If you do simple stuff, and don't overuse them, then it's probably fine, otherwise think about other options.
Abstracting away doctrine
I'm strongly in favor of this approach and I use it in almost every project.
Even though I'm one of the people who try to have the least amount of layers and indirection necessary, I do think that wrapping data persistence into your own services is a good idea. It isolates rest of your system from having to know how your data is stored.
I suggest not using Doctrine repositories/Entity manager directly. Instead, wrap them in your own services.
This makes your persistence API squeaky clean and obvious, while giving you ability to manipulate your models before they reach your business logic.
Here is an example of how I would approach your problem:
# src/AppBundle/Repository/UserRepository.php
class UserRepository
{
private $em;
public function __construct(EntityManagerInterface $em)
{
$this->em = $em;
}
public function findById($userId)
{
$user = $this->em->getRepository(User::class)->find($userId);
$this->calculateUserStatistics($user);
return $user;
}
public function save(User $user)
{
$this->em->persist($user);
$this->em->flush();
}
// ...
private function calculateUserStatistics(User $user)
{
// calculate and set statistics on user object
}
}
This approach has a number of advantages:
Decoupling from Doctrine
Your business code is no longer coupled to Doctrine, it doesn't know that Doctrine repositories/entity manager exist at all. If need arises, you can change UserRepository implementation to load users from remote API, from file on disk....from anywhere.
Model manipulation
It allows you to manipulate your models before they get to business logic, allowing you to calculate values not persisted as a field in database. Eg, to fetch them from Redis, from some API or other...
Cleaner API
It makes it really obvious what abilities your system has, making understanding easier and allowing easier testing.
Performance optimisation
It doesn't suffer from performance issues as first approach. Take the following example:
You have $eventsCount field on your User model.
If you load list of 100 users and use first approach, you would need to fire 100 queries to count number of events belonging to each user.
SELECT COUNT(*) FROM events WHERE user_id = 1;
SELECT COUNT(*) FROM events WHERE user_id = 2;
SELECT COUNT(*) FROM events WHERE user_id = 3;
SELECT COUNT(*) FROM events WHERE user_id = 4;
...
If you have your own UserRepository implementation, however, you can just make method getEventCountsForUsers($userIds) which would fire one query:
SELECT COUNT(*) FORM events WHERE user_id IN (:user_ids) GROUP BY user_id;
You can implement your own repository to include your own sql queries, Symfony have documented it pretty well in their documentation, see here.
Here's how I've done it previously using annotations (this can be done via yaml too, just check the link above)...
Entity:
namespace AppBundle\Entity;
use Doctrine\ORM\Mapping as ORM;
/**
* #ORM\Entity(repositoryClass="AppBundle\Repository\UserRepository")
*/
class User
{
// ...
private $statistic;
// ...
}
User Repository:
namespace AppBundle\Entity;
use Doctrine\ORM\EntityRepository;
class ProductRepository extends EntityRepository
{
public function getUserStats()
{
// Use query builder to build your query for stats here
}
}
Since your custom repository is extending the EntityRepository, you will still have access to Doctrines lazy load methods (find, findBy, findAll etc...)
Maybe you don't need exactly User instance.
You can use NEW() syntax in DQL.
class UserStatDTO
{
private $user;
private $statistic;
private $sum;
public function __construct(User $user, $statistic, $sum)
{
$this->user = $user;
$this->statistic = $statistic;
$this->sum = $sum;
}
public function getUser()
{
return $this->user;
}
public function getSum()
{
return $this->sum;
}
public function getStatistic()
{
return $this->statistic;
}
}
class UserRepository
{
public function getUsersWithCalculatedStat()
{
return $this->getEntityManager()->createQuery('
SELECT NEW UserStatDTO(
u, u.statistic, u.count1 + u.count2
) FROM User
')->getResult();
}
}
http://doctrine-orm.readthedocs.org/projects/doctrine-orm/en/latest/reference/dql-doctrine-query-language.html#new-operator-syntax
I'm developing a PHP CMS and I have question about flexible and «extendable» architecture.
I want to allow users to «extend» every class in system.
For example, if I have class User:
<?php
namespace MyCMSName\Extensions\Users;
class User
{
public function getUserAvatar()
{
// return file path, for example
}
}
Now I want to do something with users avatars. Return different image for each user, for example.
<?php
namespace MyCMSName\Extensions\FakeUserAvatars;
class User extends MyCMSName\Extensions\Users\User
{
public function getUserAvatar()
{
// return another path
}
}
But somewhere in system I have User object constructing. I need to create FakeUserAvatars\User, not original Users\User object.
...
$user = new (? How can I determine which class to construct?)
Should I create something about map «original class → extended class» and then generate it when installing new extensions? Then I can get class which overload original and create new instance. But what about autoloading? It seems that I should have custom function for load every class in system if I want to make it «extendable».
What do you think?
Sorry if I wrote something wrong, English isn't my native language...
I'm making a website where users can create albums.
I would like to create a default album per user.
I would like to create an Album entity and to persist it in the constructor of my User class.
Is it possible ?
I just know that the entityManager is not accessible from an Entity... That's why it's a problem for me.
Even though this technically IS possible I would strongly recommend you not to do this.
To answer your question, it is possible and it would be done like this:
class User extends FOSUser
{
/**
* #ORM\OneToMany(targetEntity="Album", cascade={"persist"})
*/
private $albums;
public function __construct()
{
$this->albums = new ArrayCollection();
$this->addAlbum(new Album());
}
public function addAlbum(Album $album)
{
$this->albums[] = $album;
}
public function getAlbums()
{
return $this->albums:
}
}
With setup like this whenever you create a new user and save it, a related album will be created together with it. I have to repeat, even though it's possible, don't do it like this.
Good solutions
There are few strategies that can be used to achieve what you want.
FOSUserBundle master
If you're not using 1.3.x version of FOSUserBundle but master, you can see that RegistrationController fires a few events. The one you're interested in is FOSUserEvents::REGISTRATION_INITIALIZE. You should create an event listener and add album to user in your listener.
FOSUserBundle 1.3.x
If you're using one of older versions, these events don't exist unfortunately and you can do it two ways.
Extend FOSUserBundle UserManager and override createUser method. You can add your album adding logic there. I would prefer this approach.
Override FOSUserBundle RegistrationController::registerAction. It can be viable option sometimes but in your case I think option 1 is better.
I have a model which is used by two modules. I don't want to duplicate the code for that model into each modules.
For example I have 2 modules. First takes the blog posts from model and print them for normal users, and the other one prints them for admin users but with a few more options (which are set in the view). I would have the same model in two places. And.. that's bad.
Just a piece of code:
<?php
namespace Blog\Model;
use Zend\Db\Adapter\Adapter;
use Zend\Db\ResultSet\ResultSet;
use Zend\Db\TableGateway\AbstractTableGateway;
class BlogTable extends AbstractTableGateway
{
protected $table = 'blog_posts';
public function __construct(Adapter $adapter)
{
$this->adapter = $adapter;
$this->initialize();
}
/**
* Gets the blog post list
*
* #return array
**/
public function fetchAll()
{
//..
}
}
So, how should I design this application?
The biggest question might be why you separate the administrative side outside of the 'Blog-Scope'. An administrative Module should only be the front-end to gather all backend-options (if that makes any sense)
However to get back into your question: you can simply include those classes from model A into model B, like use Blog\Model\BlogTable
Ultimately though i see management of a module as a responsible of the Blog-Module itself, as hinted within the first paragraph.
Edit: One thing i forgot to mention. Ideally if you decide to go the dependant approach, your module should make the BlogTable accessible through a service. That way your ModuleAdmin only has to do something like $serviceLocator->get('my-blog-table') which is a much more clean approach.
You could achieve this by having a base/shared module that provides various models, helpers etc.
But personally I would be looking to architect modules around functionality rather than user access levels.