I'm starting the serious play with doctrine.
Among my current challenges, I have to build an entity which contains an address. Let's take an example.
class Address {
private $country;
private $street;
}
class Person {
private $name;
/**
* #var Address
*/
private $address;
}
What I want to achieve is to have only one table in database, with Address being embedded in Person. Something of the like:
create table Person {
name string(255),
street string(255),
country string(255)
}
I know this is possible with ORMs like Hibernate, and I wish I could have the same behaviour with Doctrine.
How can I have an embedded one-to-one relationship with Doctrine ?
Unfortunately Doctrine's ORM doesn't support embedded objects. The only soultion I found so far is to do the mapping on your own:
class Person {
...
protected $addressName;
protected $addressStreet;
protected $addressCountry;
public function getAddress() {
return new Address($this->addressName, $this->addressStreet, $this->addressCountry);
// or Address::create(...) if you wan't to have a simple constructor
}
public function setAddress(Address $address) {
$this->addressName = $address->getName();
$this->addressStreet = $address->getStreet();
$this->addressCountry = $address->getCountry();
}
...
}
It's quite a lot of repeated code, so you can think about moving it into separate trait.
Related
Closed. This question is opinion-based. It is not currently accepting answers.
Want to improve this question? Update the question so it can be answered with facts and citations by editing this post.
Closed 3 years ago.
Improve this question
For Symfony projects using Doctrine ORM, I'm used to Anemic Domain Models, with business logic handled in Symfony services. For projects involving heavy business logic, I'm wondering if using Rich Domain Models (which I'm not familiar with) would not be a better solution. I'm not looking for a comparison of RDM vs ADM, as there is already plenty of resources available online to figure out pros and cons of each solution. I'm rather wondering if RDM are suited for Symfony projects. To make my mind, I would like a glimpse of how I should implement RMD in a real world application.
The main questions I have are:
Does RDM really suit Symfony philosophy and best practices?
Does RDM not break the SOLID principle by having models doing too much?
How dependencies are managed by the models?
I'll give a theoretical example to express my concerns in a clearer way.
Lets say we are working on a REST API. We want to implement an user registration. The user registration relies on an external service provider (using an API) and the user creation should result in the creation of some other related entities (we will use a Dummy entity here) and the populate of ElasticSearch indexes in an asynchronous manner using Symfony Messenger.
The happy flow would look like this:
Validation of the request body
Call to the external service provider API to create the user on their side
Insertion of the user data in database
Create user related entities
Populate the ElasticSearch indexes
Send a confirmation email to the user
For practical reasons, the code below is simplified and therefore may not be 100% accurate nor functional. It only aims to demonstrate the differences between the two designs.
Anemic Domain Model implementation
<?php
// src/Entity/User.php
namespace App\Entity;
class User
{
private $id;
private $externalId;
private $email;
private $plainPassword;
private $password;
private $lastName;
private $firstName;
// Include more properties, getters and setters, ...
}
<?php
// src/Service/UserRegistrationService.php
namespace App\Service;
class UserRegistrationService
{
private $passwordEncoder;
private $externalProviderClient;
private $em;
private $bus;
private $mailer;
private $elasticSearch;
private $dummyService;
public function __construct(PasswordEncoder $passwordEncoder, ExternalProviderClient $externalProviderClient, EntityManager $em, MessageBusInterface $bus, Mailer $mailer, ElasticSearchService $elasticSearch, DummyService $dummyService)
{
$this->passwordEncoder = $passwordEncoder;
$this->externalProviderClient = $externalProviderClient;
$this->em = $em;
$this->bus = $bus;
$this->mailer = $mailer;
$this->elasticSearch = $elasticSearch;
$this->dummyService = $dummyService;
}
public function register(User $user): User
{
$password = $this->passwordEncoder->encode($user->getPlainPassword());
$externalId = $this->externalProviderClient->registerUser([
'email' => $user->getEmail(),
'firstName' => $user->getFirstName(),
'lastName' => $user->getLastName(),
]);
$user
->setExternalId($externalId)
->setPassword($password)
->setPlainPassword(null)
;
$this->em->persist($user);
$this->em->flush();
$this->bus->dispatch(new UserMessage($user));
return $user;
}
public function completeRegistration(User $user)
{
$dummy = $this->dummyService->createUserDependencies($user);
$this->elasticSearch->populateUser($user);
$this->elasticSearch->populateDummy($dummy);
$this->mailer->sendConfirmationEmail($user);
}
}
<?php
// src/Controller/UserController.php
namespace App\Controller;
class UserController
{
public function registerAction(Request $request, UserRegistrationService $userRegistrationService, Serializer $serializer)
{
$user = new User();
$form = $this->createForm(UserType::class, $user);
$form->handleRequest($request);
if (!$form->isValid()) {
// Process errors
}
$userRegistrationService->register($user);
return $this->serializer->serialize($user);
}
}
As you can see, the business logic relies on a lot of services, and this is just one of the tiniest and most basic functionality the API has to offer.
Rich Domain Model implementation
<?php
// src/Entity/User.php
namespace App\Entity;
class User
{
private $id;
private $externalId;
private $email;
private $plainPassword;
private $password;
private $lastName;
private $firstName;
// Include more properties, getters and setters, ...
}
<?php
// src/Model/UserRegistration.php
namespace App\Model;
class UserRegistration
{
private $email;
private $plainPassword;
private $lastName;
private $firstName;
private function __construct(string $email, string $plainPassword, string $lastName, string $firstName)
{
$this->email = $email;
$this->plainPassword = $plainPassword;
$this->lastName = $lastName;
$this->firstName = $firstName;
}
// Getters
public static function createFromRequest(Validator $validator, Request $request)
{
$requestData = $request->request->all();
$userRegistration = new self(requestData['email'], $requestData['plainPassword'], $requestData['lastName'], $requestData['firstName']);
$violations = $validator->validate($userRegistration);
if (count($violations) > 0) {
throw new \Exception(); // handle errors
}
return $userRegistration;
}
}
<?php
// src/Model/User.php
namespace App\Model;
class User
{
private $id;
private $externalId;
private $email;
private $oassword;
private $lastName;
private $lastName;
private function __construct(string $id, string $externalId, string $email, string $password, string $lastName, string $firstName)
{
$this->id = $id;
$this->externalId = $externalId;
$this->email = $email;
$this->password = $password;
$this->lastName = $lastName;
$this->firstName = $firstName;
}
// Getters
public static function createFromUserRegistration(Validator $validator, PasswordEncoder $passwordEncoder, ExternalProviderClient $externalProviderClient, EntityManager $em, MessageBusInterface $bus, UserRegistration $userRegistration)
{
$password = $passwordEncoder->encodePassword($userRegistration->getPlainPassword());
$externalId = $externalProviderClient->registerUser([
'email' => $userRegistration->getEmail(),
'firstName' => $userRegistration->getFirstName(),
'lastName' => $userRegistration->getLastName(),
]);
$userEntity = (new \App\Entity\User())
->setExternalId($externalId)
->setEmail($userRegistration->getEmail())
->setPassword($password)
->setLastName($userRegistration->getLastName())
->setFirstName($userRegistration->getFirstName())
;
$em->persist($userEntity);
$em->flush();
$id = ;
$user = self::buildFromUserEntity($validator, $userEntity);
$bus->dispatch(new UserMessage($user));
return $user;
}
public static function buildFromUserEntity(Validator $validator, \App\Entity\User $userEntity)
{
$user = new self(
$userEntity->getId(),
$userEntity->getExternalId(),
$userEntity->getEmail(),
$userEntity->getPassword(),
$userEntity->getLastName(),
$userEntity->getFirstName()
);
$violations = $validator->validate($user);
if (count($violations) > 0) {
throw new \Exception(); // handle errors
}
return $user;
}
public function completeRegistration(EntityManager $em, ElasticSearch $elasticSearch, Mailer $mailer)
{
$dummy = new Dummy($this);
$dummy->save($em);
$this->populateElasticSearch($elasticSearch);
$dummy->populateElasticSearch($elasticSearch);
$this->sendConfirmationEmail($mailer);
}
public function populateElasticSearch(ElasticSearch $elasticSearch)
{
$this->elasticSearch->populate($this);
}
public function sendConfirmationEmail(Mailer $mailer)
{
$this->mailer->sendConfirmationEmail($this);
}
public function serialize(Serializer $serializer)
{
return $serializer->serialize($user);
}
}
<?php
// src/Controller/UserController.php
namespace App\Controller;
class UserController
{
public function registerAction(Request $request, Validator $validator, PasswordEncoder $passwordEncoder, ExternalProviderClient $externalProviderClient, EntityManager $em, MessageBusInterface $bus, Serializer $serializer)
{
$userRegistration = UserRegistration::createFromRequest($validator, $request);
$user = User::createFromUserRegistration($validator, $passwordEncoder, $externalProviderClient, $em, $bus, $userRegistration);
return $user->serialize($serializer);
}
}
The main problems I see with this implementation are:
Duplicate code between user RMD and entity
Models will get bigger and bigger with each new functionality
Managing dependencies can quickly be a mess
What are your thoughts on this? Is there things I am doing wrong?
I hope you are aware, that this is (obviously) mostly opinion-based, thus you might agree or disagree with it.
I think you have a major misunderstanding of what the Rich Domain Model and the Anemic Domain Model is. But let's start with a few assumptions:
In my understanding, entities in symfony (more precisely in Doctrine's ORM) already are models in most cases. Thus having an additional User model is just unnecessary. And even if you wanted to split, I wouldn't put the exact same fields into the model, but just have the entity as a field. If you happen to copy all of the entities functions to the model, you're doing it wrong. Since by default all fields of an entity should be private, there is no reason to not treat it as a model. (I mean, it already works with objects and not their ids in relations...).
A User model/entity should never be concerned with sending emails, IMHO, because it breaks separation of concerns. Instead there should be something modelling the process in which the email is sent. I find this article describes rather cleanly, how this works. Pay attention to the changes to Shipment and to the CheckoutService -> Checkout.
Ironically, your "Anemic Domain Model"s UserRegistrationService is quite good. The user entity is a bit anemic in that implementation though and it should probably validate the User entity, but apart from that, the service could be renamed to UserRegistration and would fit very fine into RDM. (I agree, that the validation is already being done by the form (a convenience really), but there might be validations that are not about the consistency of the user in itself, but about the user as part of the collection of users in your database / model, or something else).
To summarize: In my view, Symfony can do RDM, and rather well. But the real crux (as always) is to actually choose/design the best model.
Essentially: Anemic means you don't have one place where all things are consistently done, but instead is split in a way which risks consistency/integrity or split separation of concerns to independent units. RDM on the contrary keeps it clustered in semantically sensible places. It doesn't change the fact, that you still want separation of concerns!
Now to answer your questions:
Does RDM really suit Symfony philosophy and best practices?
why not. depends on the modelling though, which might be adapted to fit Symfony's best practices.
Does RDM not break the SOLID principle by having models doing too much?
not generally, if done right. Your implementation definitely does break SOLID, but RDM doesn't have to. Nobody is saying that having a UserRegister and UserCancel and UserUpdate service/model would be wrong. RDM is about keeping the stuff that semantically belongs together in a business process/unit also together in code (which doesn't negate separation of concern or single purpose).
How dependencies are managed by the models?
Since in my view, business processes are models, and will act as services, dependencies are just as they are handled in services. Entities on the other hand should not need services ever*. (there might be some very very special circumstances, in which case you might want to actually have a service (a factory maybe) that manages the creation/updates of the entity)
What are your thoughts on this? Is there things I am doing wrong?
your implementation is, let's say 'unfortunate' in that it isn't RDM in my understanding and (as you realize yourself) breaks SOLID all over the place. So yes to the second part of the question.
is it possible to create a relation to a generic table/class whith Doctrine?
Here is some code to make it easier to understand:
// class Log...
// TODO:
// It could be useful to have a reference to
// the element mentioned by the log, the issue is
// we don't know what kind of entity it is.
/**
* #ORM\ManyToOne(targetEntity="???")
*/
private $elementId
Maybe instead of using targetEntity I could just use an int that is the id of the element located in the unknow table.
There is no built-in possibility now.
Let me propose a work around using Doctrine Lifecycle Events :
Create 3 properties :
/*
* #ORM\Column(name="element_class", type="string")
*/
private $elementClass
/*
* #ORM\Column(name="element_id", type="integer")
*/
private $elementId
// Not mapped
private $element
public function setElement($element)
{
$this->element = $element;
$this->elementClass = get_class($element);
$this->elementId = $element->getId();
}
public function getElement()
{
return $this->element;
}
// You need these for the PostLoad event listener :
public function hydrateElementPostLoad($element)
{
$this->element = $element;
}
public function getElementClass()
{
return $this->elementClass;
}
public function getElementId()
{
return $this->elementId;
}
Then create a PostLoadListener able to hydrate the element property :
namespace AppBundle\EventListener;
use Doctrine\ORM\Event\LifecycleEventArgs;
use AppBundle\Entity\Log;
class PostLoadListener
{
public function postLoad(LifecycleEventArgs $args)
{
$entity = $args->getEntity();
if($entity instanceOf Log){
$em = $args->getEntityManager();
$entity->hydrateElementPostLoad(
$this->em->getRepository($entity->getElementClass())->findOneById($entity->getElementId())
);
}
}
}
And register this event in your services.yml :
services:
places.listener:
class: AppBundle\EventListener\PostLoadListener
tags:
- { name: doctrine.event_listener, event: postLoad }
That's also how the most famous Bundle for logging works (The Gedmo DoctrineExtensions Logger)
To retrieve all logs for an entity, create a repository method for your Log entity :
getLogs($entity)
{
return $this->_em->findBy(array(
'element_id'=>$entity->getId(),
'element_class'=>get_class($entity)
));
}
You are trying to manage some abstraction of one or more of your entities in the database level which is a headache,
Doctrine already has proposed Somme solutions to manage this kind of abstractions by using Inheritance Mapping
A mapped superclass is an abstract or concrete class that provides persistent entity state and mapping information for its subclasses, but which is not itself an entity. Typically, the purpose of such a mapped superclass is to define state and mapping information that is common to multiple entity classes.
For more information check this
I am using Symfony2 and I have a hierarchy of classes. The hierarchy is pretty simple, I have a Question (the parent) and many different sub-questions. Using Sonata, I want to be able to create different types of questions, that are sub-questions. To do so, I created a hierarchy of classes as follows :
Hippy\ScavengerHuntBundle\Entity\Question:
type: entity
table: null
inheritanceType: JOINED
discriminatorColumn:
name: subClass
type: string
discriminatorMap:
blurredMultipleChoiceQuestion: BlurredMultipleChoiceQuestion
blurredTextQuestion: BlurredTextQuestion
slidingPuzzleQuestion: SlidingPuzzleQuestion
associationQuestion: AssociationQuestion
trueOrFalseQuestion: TrueOrFalseQuestion
lettersInOrderQuestion: LettersInOrderQuestion
shortTextQuestion: ShortTextQuestion
multipleChoiceQuestion: MultipleChoiceQuestion
sentenceGapQuestion: SentenceGapQuestion
fields:
id:
type: integer
id: true
generator:
strategy: AUTO
title:
type: string
length: 255
position:
type: integer
lifecycleCallbacks: { }
And I'll show you one example of a subclass
Hippy\ScavengerHuntBundle\Entity\LettersInOrderQuestion:
type: entity
table: null
fields:
description:
type: text
lifecycleCallbacks: { }
<?php
namespace Hippy\ScavengerHuntBundle\Entity;
use Doctrine\ORM\Mapping as ORM;
/**
* LettersInOrderQuestion
*/
class LettersInOrderQuestion extends Question
{
/**
* #var string
*/
private $description;
/**
* Set description
*
* #param string $description
* #return LettersInOrderQuestion
*/
public function setDescription($description)
{
$this->description = $description;
return $this;
}
/**
* Get description
*
* #return string
*/
public function getDescription()
{
return $this->description;
}
}
At this point, everything seems to be set up properly (the database and the php classes).
Now, I want to integrate this to SonataAdmin, so I added the following in the services
sonata.admin.question:
class: Hippy\ScavengerHuntBundle\Admin\QuestionAdmin
tags:
- { name: sonata.admin, manager_type: orm, group: "Questions", label: "Question" }
arguments:
- ~
- Hippy\ScavengerHuntBundle\Entity\Question
- ~
calls:
- [ setTranslationDomain, [HippyScavengerHuntBundle]]
- [ setSubClasses, [{lettersInOrderQuestion : "Hippy\ScavengerHuntBundle\Entity\LettersInOrderQuestion"}]]
And I created a class QuestionAdmin.php
<?php
// src/Acme/DemoBundle/Admin/PostAdmin.php
namespace Hippy\ScavengerHuntBundle\Admin;
use Sonata\AdminBundle\Admin\Admin;
use Sonata\AdminBundle\Datagrid\ListMapper;
use Sonata\AdminBundle\Datagrid\DatagridMapper;
use Sonata\AdminBundle\Form\FormMapper;
use Hippy\ScavengerHuntBundle\Entity\LettersInOderQuestion;
class QuestionAdmin extends Admin
{
// Fields to be shown on create/edit forms
protected function configureFormFields(FormMapper $formMapper)
{
$subject = $this->getSubject();
var_dump($subject);
//exit();
if ($subject instanceof LettersInOrderQuestionAdmin) {
$formMapper->add('description', 'text');
}
}
// Fields to be shown on filter forms
protected function configureDatagridFilters(DatagridMapper $datagridMapper)
{
$datagridMapper
->add('title')
;
}
// Fields to be shown on lists
protected function configureListFields(ListMapper $listMapper)
{
$listMapper
->addIdentifier('title')
;
}
}
At this point, one thing that is cool is that Sonata admin seems to recognize that I'm dealing with subclasses, take a look :
My problem is that when I try to create a lettersInOrderQuestion object, it is not recognized as a lettersInOrderQuestion but only as a Question. See here :
We can see, first via the var_dump and second because the form description is not show, that the object passed is a Question and not a LettersInOrderQuestion, even though the url is
/admin/hippy/scavengerhunt/question/create?subclass=lettersInOrderQuestion
I'm running out of ideas....
Edit1:
In Question AdminClass, in the configureFormFields method, I added
var_dump($this->getSubClasses());
and the result was the following:
array (size=1)
'lettersInOrderQuestion' => string 'Hippy\ScavengerHuntBundle\Entity
ettersInOrderQuestion' (length=56)
Therefore, it looks like there is an error in the parsing of the entity class as the name gets mixed up...
First, there is a typo in your namespace in QuestionAdmin, it should probably be
use Hippy\ScavengerHuntBundle\Entity\LettersInOrderQuestion;
and not (Oder instead of Order"
use Hippy\ScavengerHuntBundle\Entity\LettersInOderQuestion;
Secondly, also in QuestionAdmin, you are mixing the Admin class and the entity class. See here, you have:
if ($subject instanceof LettersInOrderQuestionAdmin) {
it should be, according to your code:
if ($subject instanceof LettersInOrderQuestion) {
Finally, in SonataAdmin, it looks like if you put only one subclass, the class never gets active. You have to put at least two subClasses, if not, the subclass never gets active, see here :
public function hasActiveSubClass()
{
if (count($this->subClasses) > 1 && $this->request) {
return null !== $this->getRequest()->query->get('subclass');
}
return false;
}
An issue has been opened here : https://github.com/sonata-project/SonataAdminBundle/issues/1945
Sonata admin takes as second constructor argument entity class. This class is saved in private variable of parent SonataAdminClass and can not be changed. This class use Model manager to create new instance of this entity class, in your case Question.
This Question object returns admin by method getSubject();
Admin knows nothing about your intention to use entity LettersInOderQuestion.
Im seraching over and cannot find answer.
I have database role model in my application. User can have a role but this role must be stored into database.
But then user needs to have default role added from database. So i created a service:
<?php
namespace Alef\UserBundle\Service;
use Alef\UserBundle\Entity\Role;
/**
* Description of RoleService
*
* #author oracle
*/
class RoleService {
const ENTITY_NAME = 'AlefUserBundle:Role';
private $em;
public function __construct(EntityManager $em)
{
$this->em = $em;
}
public function findAll()
{
return $this->em->getRepository(self::ENTITY_NAME)->findAll();
}
public function create(User $user)
{
// possibly validation here
$this->em->persist($user);
$this->em->flush($user);
}
public function addRole($name, $role) {
if (($newrole = findRoleByRole($role)) != null)
return $newrole;
if (($newrole = findRoleByName($name)) != null)
return $newrole;
//there is no existing role
$newrole = new Role();
$newrole->setName($name);
$newrole->setRole($role);
$em->persist($newrole);
$em->flush();
return $newrole;
}
public function getRoleByName($name) {
return $this->em->getRepository(self::ENTITY_NAME)->findBy(array('name' => $name));
}
public function getRoleByRole($role) {
return $this->em->getRepository(self::ENTITY_NAME)->findBy(array('role' => $role));
}
}
my services.yml is:
alef.role_service:
class: Alef\UserBundle\Service\RoleService
arguments: [%doctrine.orm.entity_manager%]
And now I want to use it in two places:
UserController and User entity. How can i get them inside entity?
As for controller i think i just need to:
$this->get('alef.role_service');
But how to get service inside entity?
You don't. This is a very common question. Entities should only know about other entities and not about the entity manager or other high level services. It can be a bit of a challenge to make the transition to this way of developing but it's usually worth it.
What you want to do is to load the role when you load the user. Typically you will end up with a UserProvider which does this sort of thing. Have you read through the sections on security? That should be your starting point:
http://symfony.com/doc/current/book/security.html
The reason why it's so difficult to get services into entities in the first place is that Symfony was explicitly designed with the intent that services should never be used inside entities. Therefore, the best practice answer is to redesign your application to not need to use services in entities.
However, I have found there is a way to do it that does not involve messing with the global kernel.
Doctrine entities have lifeCycle events which you can hook an event listener to, see http://docs.doctrine-project.org/projects/doctrine-orm/en/latest/reference/events.html#lifecycle-events For the sake of the example, I'll use postLoad, which triggers soon after the Entity is created.
EventListeners can be made as services which you inject other services into.
Add to app/config/config.yml:
services:
example.listener:
class: Alef\UserBundle\EventListener\ExampleListener
arguments:
- '#alef.role_service'
tags:
- { name: doctrine.event_listener, event: postLoad }
Add to your Entity:
use Alef\UserBundle\Service\RoleService;
private $roleService;
public function setRoleService(RoleService $roleService) {
$this->roleService = $roleService;
}
And add the new EventListener:
namespace Alef\UserBundle\EventListener;
use Doctrine\ORM\Event\LifecycleEventArgs;
use Alef\UserBundle\Service\RoleService;
class ExampleListener
{
private $roleService;
public function __construct(RoleService $roleService) {
$this->roleService = $roleService;
}
public function postLoad(LifecycleEventArgs $args)
{
$entity = $args->getEntity();
if(method_exists($entity, 'setRoleService')) {
$entity->setRoleService($this->roleService);
}
}
}
Just keep in mind this solution comes with the caveat that this is still the quick and dirty way, and really you should consider redesigning your application the proper way.
Thanks to Kai's answer above which answer to the question, but it's not compatible with symfony 5.x .
It's good to precise it's a bad practice, but required in some special case like legacy code or a bad DB design (as a temporary solution before schema migration)
As in my case, I use this code with a mailer and translator, which introduce an issue with the private property if Symfony >= 5.3 , so here the solution for recent version of symfony:
in config/services.yaml:
services:
Alef\UserBundle\EventListener\ExampleListener:
tags:
- { name: doctrine.event_listener, event: postLoad }
ExampleListener:
namespace Alef\UserBundle\EventListener;
use Doctrine\ORM\Event\LifecycleEventArgs;
use Alef\UserBundle\Entity\Role;
class ExampleListener
{
public function postLoad(LifecycleEventArgs $postLoad): void
{
$entity = $postLoad->getEntity();
if ($entity instanceof User) {
$repository = ;
$entity->roleRepository(
$postLoad->getEntityManager()->getRepository(Role::class)
);
}
}
}
And in your Entity (or in a trait if you use it in more than one entity):
use Alef\UserBundle\Service\RoleService;
/** #internal bridge for legacy schema */
public function roleRepository(?RoleRepository $repository = null) {
static $roleRepository;
if (null !== $repository) {
$roleRepository = $repository;
}
return $roleRepository;
}
public function getRoleByName($name) {
return $this->roleRepository()->findBy(array('name' => $name));
}
I'm programming in PHP procedurally (is this even a word?) for about five years now and decided to try an OOP approach but ran into some concept/design problems. Let's say you have some modules in the program, every module has the possibility to list, add, edit and delete an entity. An entity can be..dunno, user, client, product etc.
How would you design the classes to manipulate these entityes?
Two possibilities came in my mind:
create classes for every entity with methods like getUsersList, addUser, editUser, delUser
This approach seems resource consumingbecause in the script for the listing you only need the getUsersList and maybe delUser methods, while in the add user popup script, you only need the addUser method and in the edit user popup script only the editUser method. So, you have to instanciate an object and only use two or one of it's methods...
create general classes: listing, add, edit and delete and extend them for every entity this way you only have to instanciate one class at a time (the one you really need)
Thanks in advance,
I would create an interface defining your list, add, edit, and delete methods. This gives you a class "template". If your classes (User, Client, Product, etc.) implement this interface, then the methods in the interface must be defined in those classes.
This will give you a similar "API" to access all the functionality of every class that implements your interface. Since each of your listed objects contains different data, the details of the methods will be different, and thus separate, but the interface will be the same.
Aside:
Your inclusion of "list" in your list of methods concerns me a little. It seems to imply that you are seeing your objects as collections of Users, Clients, Products, etc, where there should most likely be a User class that represents a single user, a Client class that represents a single client, etc.
On the other hand, "list" may be handled as a static method - a method that can be called without an instance of the class.
$bob = new User('bob');
$bob->add(); // Adds bob to the database
$fred = new User('fred');
$fred->add(); // Adds fred to the database
$users = User::list(); // Gives an array of all the users in the database
That's how I would handle things, anyway.
You will need to create a solid architecture and framework for managing your data model. This is not easy and will only get more complex as the data model grows. I would highly recommend using a PHP framework (Symfony, CakePHP, etc), or at least, an ORM (Doctrine, Propel, etc).
If you still want to roll your own, I would start with an architecture similar to below.
You will want a DbRecord class that is used for individual record operations (like saving, deleting, etc). This DbRecord class will be extended by specific entities and will provide the foundation for basic entity operations.
class DbRecord {
public function save() {
// save logic (create or update)
}
public function delete() {
// delete logic
}
// other record methods
}
class User extends DbRecord {
private $name;
private $email;
public function setName($name_) {
$this->name = $name_;
}
public function setEmail($email_) {
$this->email = $email_;
}
}
From which, you can perform individual record operations:
$user = new User();
$user->setName('jim');
$user->setEmail('jim#domain.com');
$user->save();
You will now want a DbTable class that is used for bulk operations on the table (like reading all entities, etc).
class DbTable {
public function readAll() {
// read all
}
public function deleteAll() {
// delete all logic
}
public function fetch($sql) {
// fetch logic
}
// other table methods
}
class UserTable extends DbTable {
public function validateAllUsers() {
// validation logic
}
// ...
}
From which, you can perform bulk/table operations:
$userTable = new UserTable();
$users = $userTable->readAll();
foreach ($users as $user) {
// etc
}
Code architecture is the key to a website scaling properly. It is important to divide the data model into the appropriate classes and hierarchy.
Again, as your website grows, it can get very complicated to manage the data model manually. It is then when you will really see the benefit of a PHP framework or ORM.
NOTE: DbRecord and DbTable are arbitrary names - use w/e name you like.
Use your first method, where you create a reusable object with methods. It is not a waste of time as you only code it once.
class User {
function __construct() { /* Constructor code */ }
function load($id) { ... }
function save() { ... }
function delete() { ... }
}
You're on the right track with 'general classes' (also called base classes, or abstract classes in case their behaviour NEEDS to be complemented by child classes before they can be put to use).
The OOP approach would be to put all behavior that is common to all entities in the base classes.
If you use something akin to ActiveRecord, you already have a general (abstract) interface for create-update-delete operations. Use that to your advantage, and let your base classes operate ONLY on those interface methods. They don't need to know they are updating a Product, or a a User, they just need to know they can call the update() method on an entity.
But even without using something quite feature-heavy like an AR framework (check out Doctrine if you're interested in a very flexible ORM..) you can use interfaces to abstract behavior.
Let me give you a more elaborate example...
/**
* Interface for all entities to use
*/
interface Entity {
static function newEntity();
static function fetch($id);
function save();
function setProperties(array $properties);
function delete();
}
/**
* A concrete product entity which implements the interface
*/
class Product implements Entity {
public $productId;
public $name;
public $price;
public $description;
/**
* Factory method to create a new Product
*
* #param integer $id Optional, if you have auto-increment keys you don't need to set it
* #return Product
*/
public static function newEntity($id=NULL) {
$product = new Product();
$product->productId = $id;
return $product;
}
/**
* Factory method to fetch an existing entity from the database
*
* #param integer $id
* #return Product
*/
public static function fetch($id) {
// make select with supplied id
// let $row be resultset
if (!$row) {
return NULL; // you might devise different strategies for handling not-found cases; in this case you need to check if fetch returned NULL
}
$product = new Product();
$product->productId = $id;
$product->name = $row['name'];
$product->price = $row['price'];
$product->description = $row['description'];
return $product;
}
/**
* Update properties from a propreties array
* #param array $properties
* #return void
*/
public function setProperties(array $properties) {
$this->name = $properties['name'];
$this->price = $properties['price'];
$this->description = $properties['description'];
}
public function save() {
// save current product properties to database
}
public function delete() {
// delete product with $this->productId from database
}
}
/**
* An abstract CRUD controller for entities
*/
abstract class EntityCrudController {
protected $entityClass = 'UNDEFINED'; // Override this property in child controllers to define the entity class name
protected $editTemplate = NULL; // Override this to set an edit template for the specific entity
protected $templateEngine; // Pseudo-Templating engine for this example
/**
* Display the edit form for this entity
* #param integer $entityId
* #return string
*/
public function editAction($entityId) {
// Fetch entity - this is not the most clean way to fetch, you should probably consider building a factory that encapsulates this.
$entity = call_user_func($this->entityClass, 'fetch', $entityId);
// Assign entity to your edit template, in this example I'm assuming we're using a template engine similar to Smarty
// You can generate the HTML output in any other way you might like to use.
$this->templateEngine->setTemplate($this->editTemplate);
$this->templateEngine->assign('entity', $entity);
return $this->template->render();
}
/**
* Update an existing entity
*
* #param integer $entityId
* #param array $postArray
* #return string
*/
public function updateAction($entityId, array $formArray) {
// Be sure to validate form data first here, if there are errors call $this->editAction() instead and be sure to set some error information
$entity = call_user_func($this->entityClass, 'fetch', $entityId);
$entity->setProperties($formArray);
$entity->save();
// Again, using our imaginary templating engine to display...
$this->templateEngine->setTemplate($this->editTemplate);
$this->templateEngine->assign('entity', $entity);
$this->templateEngine->assign('message', 'Saved successfully!');
return $this->template->render();
}
// Devise similar generic methods for newAction/insertAction here
}
/**
* Concrete controller class for products
* This controller doesn't do much more than extend the abstract controller and override the 2 relevant properties.
*/
class ProductCrudController extends EntityCrudController {
protected $entityClass = 'Product';
protected $editTemplate = 'editProduct.tpl';
}
// Usage example:
// Display edit form:
$controller = new ProductCrudController();
$htmlOutput = $controller->editAction(1);
// Save product:
$htmlOutput = $controller->updateAction(1, array('name' => 'Test Product', 'price' => '9.99', 'description' => 'This is a test product'));
Of course, there is much to improve.. e.g. you generally don't want to make a query everytime you call fetch() on an entity, but instead only query once and store the resulting object in an IdentityMap, which also ensures data integrity.
Hope this helps, got a bit more than I intended, but I think it's commendable you try to tackle this without throwing a framework on the problem :)