I have the following Notification entity:
As you can see, there is a field called "objectId" where I want to store the related object id depending on the notification type. Then I add the notification to an email queue. When the queue gets processed I have a problem to fetch the object from the specific service class. For example:
Notification Type 1: UserService::getUser($objectId)
Notification Type 2: CompanyService::getCompany($objectId)
So how could I define that relations without having troubles to add more and more notification types. It feels bad to inject all the needed services and handle it through thousands of "if this than that" :)
If you injected the object instead of the id, you wouldn't need to call an additional service inside the notification to get the appropriate instance.
If the Notification doesn't need to know about what kind of object its using, just depend on an interface that both User and Company implement, and inject those objects directly into Notification.
E.g.:
interface EmailNotifiableEntity {
function getLabel()
function getEmailAddress()
}
class User implements EmailNotifiableEntity {
public function getLabel() {
return $this->getName() . " " . $this->getFullName();
}
public function getEmailAddress() {
return this->getEmailAddress();
}
}
class Company implements EmailNotifiableEntity {
public function getLabel() {
return $this->getCompanyName();
}
public function getEmailAddress() {
return this->getNotificationsEmail();
}
}
class Notification {
public function __construct(EmailNotifiableEntity $entity) {
$this->entity = $entity;
}
public function send() {
$address = $entity->getEmailAddress();
$label = $entity->getLabel();
// do your thing to send your notification
}
(Implementation is a bit bare-bones, so take what you need and build upon it). This way, when you instantiate your Notification you inject the depended upon entity without knowing its specific kind.
Related
We're trying to find the best way to implement dependency injection in a Symfony project with a quite specific problematic.
At user level, our application rely on an "Account" doctrine entity which is loaded with the help of the HTTP_HOST global against a domain property (multi-domain application). Going on the domain example.domain.tld will load the matching entity and settings.
At the devops level, we also need to do batch work with CLI scripts on many accounts at the same time.
The question we are facing is how to write services that will be compatible with both needs?
Let's illustrate this with a simplified example. For the user level we have this and everything works great:
Controller/FileController.php
public function new(Request $request, FileManager $fileManager): Response
{
...
$fileManager->addFile($file);
...
}
Service/FileManager.php
public function __construct(AccountFactory $account)
{
$this->account = $account;
}
Service/AccountFactory.php
public function __construct(RequestStack $requestStack, AccountRepository $accountRepository)
{
$this->requestStack = $requestStack;
$this->accountRepository = $accountRepository;
}
public function createAccount()
{
$httpHost = $this->requestStack->getCurrentRequest()->server->get('HTTP_HOST');
$account = $this->accountRepository->findOneBy(['domain' => $httpHost]);
if (!$account) {
throw $this->createNotFoundException(sprintf('No matching account for given host %s', $httpHost));
}
return $account;
}
Now if we wanted to write the following console command, it would fail because the FileManager is only accepting an AccountFactory and not the Account Entity.
$accounts = $accountRepository->findAll();
foreach ($accounts as $account) {
$fileManager = new FileManager($account);
$fileManager->addFile($file);
}
We could tweak in the AccountFactory but this would feel wrong...
In reality this is even worse because the Account dependency is deeper in services.
Does anyone have an idea how to make this properly ?
As a good practice, you should create an interface for the FileManager and set this FileManagerInterface as your dependency injection (instead of FileManager).
Then, you can have different classes that follow the same interface rules but just have a different constructor.
With this approach you can implement something like:
Service/FileManager.php
interface FileManagerInterface
{
// declare the methods that must be implemented
public function FileManagerFunctionA();
public function FileManagerFunctionB(ParamType $paramX):ReturnType;
}
FileManagerInterface.php
class FileManagerBase implements FileManagerInterface
{
// implement the methods defined on the interface
public function FileManagerFunctionA()
{
//... code
}
public function FileManagerFunctionB(ParamType $paramX):ReturnType
{
//... code
}
}
FileManagerForFactory.php
class FileManagerForFactory implements FileManagerInterface
{
// implement the specific constructor for this implementation
public function __construct(AccountFactory $account)
{
// your code here using the account factory object
}
// additional code that is needed for this implementation and that is not on the base class
}
FileManagerAnother.php
class FileManagerForFactory implements FileManagerInterface
{
// implement the specific constructor for this implementation
public function __construct(AccountInterface $account)
{
// your code here using the account object
}
// additional code that is needed for this implementation and that is not on the base class
}
Ans last but not least:
Controller/FileController.php
public function new(Request $request, FileManagerInterface $fileManager): Response
{
// ... code using the file manager interface
}
Another approach that also looks correct is, assuming that FileManager depends on an AccountInstance to work, changes could be made to your FileManager dependency to have the AccountInstance as a dependency instead of the Factory. Just Because in fact, the FileManager does not need the factory, it needs the result that the factory generates, so, automatically it is not FileManager's responsibility to carry the entire Factory.
With this approach you will only have to change your declarations like:
Service/FileManager.php
public function __construct(AccountInterface $account)
{
$this->account = $account;
}
Service/AccountFactory.php
public function createAccount():AccountInterface
{
// ... your code
}
For historical reasons, my pattern of running databases using Symfony is mixed. That is, the query uses DBAL and the insert uses ORM. Now you need to write a lot of data to the database. The flush in ORM can help me achieve business at the lowest cost.
All flush operations have been removed from the project. Put it in the __destruct of the controller.
However, doing so will cause DBAL to not find the latest changed data. Of course, these data ORMs can be obtained normally.
This is a very difficult problem. I hope to get guidance.
class BaseController extends Controller
{
public function __destruct()
{
$this->getDoctrine()->getManager()->flush();
}
public function indexAction()
{
$model = new CompanyModel();
$model->install(['company_name' => '1234']);
$model->update(['company_name' => 'abcd'], $model->lastInsertId);
}
}
class CompanyModel extends BaseController
{
public function validate($data, $id = false)
{
$this->entityManager = $this->getDoctrine()->getManager();
if(empty($id)){
$this->company_class = new Company();
}else{
if(!$this->is_exist($id)){
return false;
}
$this->company_class = $this->entityManager->getRepository(Company::class)->find($id);
}
if(array_key_exists('company_name', $data)){
$this->company_class->setCompanyName($data['company_name']);
}
if(self::$error->validate($this->company_class)){
return false;
}
return true;
}
public function insert($data)
{
if(!$this->validate($data)){
return false;
}
$this->company_class->setCreateAt(new \DateTime());
$this->entityManager->persist($this->company_class);
//$this->entityManager->flush();
$this->lastInsertId = $this->company_class->getId();
return true;
}
public function update($data, $id)
{
if(empty($id)){
self::$error->setError('param id is not null');
return false;
}
if(!$this->validate($data, $id)){
return false;
}
$this->company_class->setUpdateAt(new \DateTime());
//$this->entityManager->flush();
return true;
}
public function is_exist($id)
{
return $this->get('database_connection')->fetchColumn('...');
}
}
The final result of executing indexAction company_name is 1234; $ model-> update() was not executed successfully. The reason is that the $this-> is_exist() method that took the DBAL query did not find the ORM insert but did not flush the message.
Unchanging conditions,run
$this->entityManager->getRepository(Company::class)->find($id);
Is successful。
The problem is not the entity manager or dbal, as far as I can tell, but the usage of an anti-pattern, which I would call ... entanglement. What you should strive for is separation of concerns. Essentially: Your "CompanyModel" is an insufficient and bad wrapper for the EntityManager and/or EntityRepository.
No object should know about the entity manager. It should only be concerned with holding the data.
The entity manager should be concerned with persistence and ensuring integrity.
The controller is meant to orchestrate one "action", that can be adding one company, editing one company, batch-importing/updatig many companies.
Services can be implemented, when actions become to business-logic-heavy or when functionality is repeated.
(Note: the following code samples could be made way more elegant with using all the features that symfony provide, like ParamConverters, the Form component, the Validation component, I usually wouldn't write code this way, but I assume everything else would go way over your head - no offence.)
handling actions in the controller
controller actions (or service actions, really) are when you look at your problem from the task perspective. Like "I want to update that object with this data"). That's when you fetch/create that object, then give it the data.
use Doctrine\ORM\EntityManagerInterface;
class BaseController extends Controller {
public function __construct(EntityManagerInterface $em) {
$this->em = $em;
}
public function addAction() {
$company = new Company(['name' => '1234']); // initial setting in constructor
$this->em->persist($company);
// since you have the object, you can do any changes to it.
// just change the object
$company->update(['name' => 'abcd']); // <-- don't need id
// updates will be flushed as well!
$this->em->flush();
}
public function editAction($id, $newData) {
$company = $this->em->find(Company::class, $id);
if(!$company) {
throw $this->createNotFoundException();
}
$company->update($newData);
$this->em->flush();
}
// $companiesData should be an array of arrays, each containing
// a company with an id for update, or without an id for creation
public function batchAction(array $companiesData) {
foreach($companies as $companyData) {
if($companyData['id']) {
// has id -> update existing company
$company = $this->em->find(Company::class, $companyData['id']);
//// optional:
// if(!$company) { // id was given, but company does not exist
// continue; // skip
// // OR
// $company = new Company($companyData); // create
// // OR
// throw new \Exception('company not found: '.$companyData['id']);
// }
$company->update($companyData);
} else {
// no id -> create new company
$company = new Company($companyData);
$this->em->persist($company);
}
}
$this->em->flush(); // one flush.
}
}
the base controller should handle creating objects, and persisting it, so very basic business logic. some would argue, that some of those operations should be done in an adapted Repository for that class, or should be encapsulated in a Service. And they would be right, generally.
the entity handles it's internal state
Now, the Company class handles its own properties and tries to stay consistent. You just have to make some assumptions here. First of all: the object itself shouldn't care if it exists in the database or not. it's not its purpose! it should handle itself. Separation of concerns! The functions inside the Company entity should concern simple business logic, that concerns its INNER state. It doesn't need the database, and it should not have any reference to the database, it only cares about it's fields.
class Company {
/**
* all the database fields as public $fieldname;
*/
// ...
/**
* constructor for the inital state. You should never want
* an inconsistent state!
*/
public function __construct(array $data=[]) {
$this->validate($data); // set values
if(empty($this->createAt)) {
$this->createAt = new \DateTime();
}
}
/**
* update the data
*/
public function update(array $data) {
$this->validate($data); // set new values
$this->updateAt = new \DateTime();
}
public function validate(array $data) {
// this is simplified, but you can also validate
// here and throw exceptions and stuff
foreach($array as $key => $value) {
$this->$key = $value;
}
}
}
some notes
Now, there should be NO use case, where you get an object to persist and at the same time an update - with an id - that refers to the new object ... unless that object was given the id beforehand! HOWEVER. If you persist an object, that has an ID and you call $this->em->find(Company::class, $id) you would get that object back.
if you have many relations, there are always good ways to solve this problem without destroying separation of concerns! you should never inject an entity manager into an entity. the entity should not manage its own persistence! nor should it manage the persistence of linked objects. handling persistence is the purpose of the entity manager or entity repository. you should never need a wrapper around an object just to handle that object. be careful not to mix responsibilities of services, entities (objects) and controllers. In my example code, I have merged services and controllers, because in simple cases, it's good enough.
I am using an interface to handle different payment options.
The thing is in my controller I get the bank slug but based on that slug I decide which class Bank in my case to use to process the payment. My controller:
public function sendPayment($request, PayBank $bank_transacton)
{
here I want to reference the SentToBank interface and
send payment class that I get in return from checkBankImplementation function//
SendToBank()->sendLoanApplication($bank_transaction);
}
method to check for the slug so I know which payment service to sent to the interface:
public static function checkBankImplementation($bank_slug) {
switch ($bank_slug) {
case "firstbank":
return new app\FirstBank\Client();
break;
case "secondbank":
return new app\SecondBank\Client();
break;
default:
return null;
}
}
my interface :
<?php
namespace App\Banks;
interface SendToBank {
public function sendLoanApplication($bank_transaction);
}
1- your app\FirstBank\Client() and app\SecondBank\Client() needs to implement the SendToBank interface.
2- From your controller, you have to create the proper object using your static method.
3- Call the sendLoanApplication method on that object
Something like that:
public function sendPayment($request, PayBank $bank_transacton)
{
$bankClient = Class::checkBankImplementation($bank_slug); // You need to change the class name with the one actually implementing the checkBandImplementation method and extract the proper $bank_slug from the request
$bankClient->sendLoanApplication($bank_transaction);
}
I have following classes
Interface
interface IRole {
public function All();
}
In below class, I will also write some logic before sending the data to Database layer and some code after data is retrieved from Database class
class RoleBL implements IRole {
public function All() {
return (new RoleDb())->All();
}
}
Database class
class RoleDb {
public function All() {
$Roles = \App\Models\Role\RoleModel
::all();
return $Roles;
}
}
Below is my Controller Action Method
class MembershipController extends \App\Http\Controllers\BaseController
{
private $role;
public function __construct(IRole $_role) {
$this->role = $_role;
parent::__construct();
}
public function create() {
$RoleTypes = $this->role->All();
return view('Membership.Create', array('Roles' => $RoleTypes));
}
}
Can somebody help me how can I stop direct access to RoleDb class? It's access modifier is public right now.
You should not even try to do that. At PHP it will look more like hacks. Just keep one code style and if you work in team then write some guidelines.
Keep all your objects in separate layers based on their purpose. Database related objects in data access layer. Domain objects in domain layer.
Models at domain layer represent all business objects that business talk about (role, customer, comment, payment, etc.) and all related actions on them (user->register, user->assignRole, blog->postArticle, etc.). These models hold business requirements, rules.
Models at data access layer represents all objects that persists state of those business objects and finds them in specific ways (blog->getUnreadPosts, user->findAdministrators, etc.).
If by RoleBL you mean role business logic then it should be part of domain (your business requirements). These objects are persisted/retrieved by DAL.
Your business objects should not know anything about data access layer. (new RoleDb())->All(); means reading from database. To fix this you can create separate read model at DAL for querying your business objects. That means there will be separate model (e.g. RoleDataModel) at DAL for designing query side based on business/application needs.
namespace Domain
{
class Role
{
private $id;
private $title;
/* etc. */
public function getId()
{
return $this->id;
}
}
class Person
{
private $roles = [];
/* etc. */
public function assignRole(Role $role)
{
// person already promoted?
// can person be promoted ?
// if you promote person then it might not be required to add instance af this $role
// to $this->roles array, just add identifier from this role
// If you would like to respond on what did just happen
// at this point you can return events that describe just that
}
public function getRoles()
{
return $this->roles;
}
}
}
namespace DAL
{
class Person
{
function storePerson(Person $person)
{
// here you can use eloqueent for database actions
}
function getAllAdministrators()
{
}
}
}
There will be separate Person class for eloquent. Use that just for data manipulation. Map data from eloquent objets to Data Transfet Objects or your Business Layer Objects. DTOs can be more specific to your other layers like UI where you might not need to show everything that BLOs contains. DTOs for your UI will model everything your UI needs.
Get familiar with some of the DDD and overall programming concepts. I should be able to look up something that will fit your needs.
I work with Redis loaded as a service to inject followers to a entity. So i've a entity like User that has a method like getFollowers. I don't want to mix service with entities, so I make a listener that subscribe to postLoad events in Doctrine.
The question is how call the service only when I call the getFollowers method.
My code...
EventListener:
public function postLoad(LifecycleEventArgs $eventArgs)
{
$redisService = get the service loaded with DIC in constructor.
if ($eventArgs->getEntity() instanceof User) {
$user = $eventArgs->getEntity();
$user->setFollowers($redisService->getFollowers($user));
}
}
User entity:
public function setFollowers(Array $followers) {
$this->followers = $followers
}
My problem is that on every load of class user, the RedisService is called and loaded, and I'd like to call the service ONLY on $user->getFollowers
Finally I get the answer...
In my listener postLoad I assign a closure to property of object:
$socialGraph = $this->socialGraph;
$getFollowers = function() use ($socialGraph, $user) {
return $socialGraph->getFollowers($user->getId());
};
$user->setFans($getFollowers);
Now, in my object it's possible to call a method into a property with:
public function getFans()
{
return call_user_func($this->fans);
// another way
return $this->fans->__invoke();
}
Wrap it as singleton. Something like that?
if (is_callable($this->_lazyLoad)) {
$this->_lazyLoad = $this->_lazyLoad($this);
}
return $this->_lazyLoad;