I have a business rule such as this :
If a JobSeeker wants to apply to a Vacancy, make sure that the Resume used in application is completed and that JobSeeker hadn't
applied to that Vacancy already. If the condition is satisfied, then
the application will be written in a JobApplicatiion.
This is what I came up with :
JobSeeker.php
class JobSeeker {
private $applications;
private $resume;
/** Other irrelevant props **/
public function apply(Vacancy $vacancy, Resume $resume) {
// Business rule #1
if(!$resume->isCompleted()) throw new \Exception('Resume '.$resume->getTitle().' is incomplete.');
// Business rule #2
$alreadyApplied = array_filter($this->applications->toArray(), function(JobApplication $application) use($vacancy) {
return $application->getVacancy() === $vacancy;
});
if($alreadyApplied) throw new \Exception('Vacancy '.$vacancy->getTitle().' is already applied');
// If both rules passed, then create a JobApplication
$application = new JobApplication($this, $vacancy, $resume);
$this->applications->add($application);
return $application;
}
}
JobApplication.php
class JobApplication {
private $applicant;
private $vacancy;
private $resume;
public function __construct(JobSeeker $applicant, Vacancy $vacancy, Resume $resume) {
$this->applicant = $applicant;
$this->vacancy = $vacancy;
$this->resume = $resume;
}
}
If I was to expect that everyone would just use
$jobApplication = $jobSeeker->apply($vacancy, $jobSeeker->getResume());
Then there's no problem.
The problem arise when someone do this
$jobApplication = new JobApplication($jobSeeker, $vacancy, $resume);
The second example will bypass the business rule validation.
It did occurred to me to separate the rule checking to a different method :
JobSeeker.php
class JobSeeker {
public function canApply() {
// Here goes those 2 business rules mentioned
}
public function apply(Vacancy $vacancy, Resume $resume) {
if($this->canApply($vacancy, $resume)) {
return new JobApplication($this, $vacancy, $resume);
}
}
}
JobApplication.php
class JobApplication {
public function __construct(JobSeeker $jobSeeker, Vacancy $vacancy, Resume $resume) {
if($jobSeeker->canApply($vacancy, $resume)) {
// Same as before
}
}
}
While the second approach guarantees the business rule constraint, it's very redundant and still does not provides the expected result.
$jobApplication = new JobApplication($jobSeeker, $vacancy, $resume);
I need an insight in this.
Thanks !
Depending how you do it you have 2 aggregate roots as I see it
JobSeeker
Vacancy
Resume is an like a profile for an user
Well DDD likes to uses services, for almost everything.
So we have the JobSeekerApplicaitonService this services will be used for the external world.
On the JobSeekerApplicaitonService I would add the method apply
public function apply(JobSeeker $jobSeeker, Vacancy $vacancy);
First we check if the bussiness rules are met.
ie.
$jobSeeker->getResume()->isCompleted();
This check throws an error if it is not completed.
Next we make another function at the JobSeekerApplicaitonService which checks if an JobSeeker already has applied, can also be used for the view to let the user already see he has applied for example.
public function hasApplied(JobSeeker $jobSeeker, Vacancy $vacancy);
But this method can now be used in our apply function
$this->hasApplied($jobSeeker, $vacancy);
Again throw an exception when already applied.
You can now savely reutrn the new JobApplication. Although I would say the JobSeekerApplicaitonService repository and create it there, so it is saved in the db because that is what an application service is, a delegator.
Code
class JobSeekerApplicaitonService {
public function apply(JobSeeker $jobSeeker, Vacancy $vacancy) {
if ($jobSeeker->getResume()->isCompleted()) {
// throw exception
} elseif ($this->hasApplied($jobSeeker, $vacancy)) {
// throw exception
}
// save logic or something else you want
}
public function hasApplied(JobSeeker $jobSeeker, Vacancy $vacancy) {
// your check, I would now use the JobApplicationRepository
return false;
}
}
Your first examples of JobSeeker & JobApplication are correct. The JobSeeker.apply method is acting as the factory for JobApplications:
job_application = job_seeker.apply(vacancy, resume)
Looks good.
The following statement, however, doesn't make much sense:
$jobApplication = new JobApplication($jobSeeker, $vacancy, $resume);
Considering the real world, have you ever seen a JobApplication randomly burst into existence out of thin air and land on your desk? I haven't :) In most cases, one entity is created from another:
employee = company.hire(person)
invoice = employee.create_invoice(customer, invoice_terms)
etc...
If you see an entity being 'new-ed' in a command handler, it should raise an eyebrow.
Related
So I have a complicated onboarding process that does several steps. I created a class that handles the process but I've added a few more steps and I'd like to refactor this into something a bit more manageable. I refactored to use Laravel's pipeline, but feel this may not be the best refactor due to the output needing to be modified before each step.
Here is an example before and after with some pseudo code.
before
class OnboardingClass {
public $user;
public $conversation;
public function create($firstName, $lastName, $email){
// Step 1
$user = User::create();
// Step 2
$conversation = Conversation::create(); // store information for new user + existing user
// Step 3
$conversation->messages()->create(); // store a message on the conversation
// Step 4
// Send api request to analytics
// Step 5
// Send api request to other service
return $this;
}
}
after
class OnboardingClass{
public $user;
public $conversation;
public function create($firstName, $lastName, $email){
$data = ['first_name' => $firstName, ...]; // form data
$pipeline = app(Pipeline::Class);
$pipeline->send($data)
->through([
CreateUser::class,
CreateNewUserConversation::class,
AddWelcomeMessageToConversation::class,
...
])->then(function($data){
// set all properties returned from last class in pipeline.
$this->user = $data['user'];
$this->conversation = $data['conversation'];
});
return $this;
}
}
Now within each class I modify the previous data and output a modified version something like this
class CreateUser implements Pipe {
public function handle($data, Closure $next) {
// do some stuff
$user = User::create():
return $next([
'user' => $user,
'other' => 'something else'
]);
}
}
In my controller I am simply calling the create method.
class someController() {
public function store($request){
$onboarding = app(OnboardingClass::class);
$onboarding->create('John', 'Doe', 'john#example.com');
}
}
So the first pipe receives the raw form fields and outputs what the second pipe needs to get the job done in its class, then the next class outputs the data required by the next class, so on and so forth. The data that comes into each pipe is not the same each time and you cannot modify the order.
Feels a bit weird and I'm sure there is a cleaner way to handle this.
Any design pattern I can utilize to clean this up a bit?
I think you could try using Laravel Service Provider, for example, you could build a login service provider; or Event & Listener, for example, you could build an listener for login and triggers a event to handle all the necessary logics. Can't really tell which one is the best since outcome is the same and it makes same amount of network requests, but it's more on personal preferences
while learning DDD and cqrs, here is something I need to clarify. In a shopping context, I have a customer which I believe is my aggregate root and I would like to implement the simple use-case of change customer name.
Here is my implementation take on this using DDD/CQRS as much as I know.
My concerns are
for validation, should the command also validate the input to make it conform with the value object or is it okay to leave it to handler?
is my overall flow alright or am I heavily missing somewhere?
If this flow is right, I see that Customer Aggregate root will be a huge class with numerous functions like changeName, changeAddress, changePhoneNumber, deleteSavedPaymentMethod and so on.
It will become a god class, and that seems a bit odd to me, is it actually the right way of DDD aggregate root implementation?
// Value Object
class CustomerName
{
private string $name;
public function __construct(string $name)
{
if(empty($name)){
throw new InvalidNameException();
}
$this->name = $name;
}
}
// Aggregate Root
class Customer
{
private UUID $id;
private CustomerName $name;
public function __construct(UUID $id, CustomerName $name)
{
$this->id = $id;
$this->name = $name;
}
public function changeName(CustomerName $oldName, CustomerName $newName) {
if($oldName !== $this->name){
throw new InconsistencyException('Probably name was already changed');
}
$this->name = $newName;
}
}
// the command
class ChangeNameCommand
{
private string $id;
private string $oldName;
private string $newName;
public function __construct(string $id, string $oldName, string $newName)
{
if(empty($id)){ // only check for non empty string
throw new InvalidIDException();
}
$this->id = $id;
$this->oldName = $oldName;
$this->newName = $newName;
}
public function getNewName(): string
{
return $this->newName; // alternately I could return new CustomerName($this->newName)] ?
}
public function getOldName(): string
{
return $this->oldName;
}
public function getID(): string
{
return $this->id;
}
}
//the handler
class ChangeNameHandler
{
private EventBus $eBus;
public function __construct(EventBus $bus)
{
$this->eBus = $bus;
}
public function handle(ChangeNameCommand $nameCommand) {
try{
// value objects for verification
$newName = new CustomerName($nameCommand->getNewName());
$oldName = new CustomerName($nameCommand->getOldName());
$customerTable = new CustomerTable();
$customerRepo = new CustomerRepo($customerTable);
$id = new UUID($nameCommand->id());
$customer = $customerRepo->find($id);
$customer->changeName($oldName, $newName);
$customerRepo->add($customer);
$event = new CustomerNameChanged($id);
$this->eBus->dispatch($event);
} catch (Exception $e) {
$event = new CustomerNameChangFailed($nameCommand, $e);
$this->eBus->dispatch($event);
}
}
}
//controller
class Controller
{
public function change($request)
{
$cmd = new ChangeNameCommand($request->id, $request->old_name, $request->new_name);
$eventBus = new EventBus();
$handler = new ChangeNameHandler($eventBus);
$handler->handle($cmd);
}
}
PS. some classes like UUID, Repo etc skipped for brevity.
should the command also validate the input to make it conform with the value object or is it okay to leave it to handler?
"Is it okay" -- of course; the DDD police are not going to come after you.
That said, you may be better off in the long run designing your code so that the different concepts are explicit, rather than implicit.
For example:
$cmd = new ChangeNameCommand($request->id, $request->old_name, $request->new_name);
What this tells me -- a newcomer to your code base -- is that ChangeNameCommand is an in memory representation of the schema of your HTTP API, which is to say it is a representation of your contract with your consumers. Customer contracts and domain models don't change for the same reasons, so it may be wise to make the separation of the two explicit in your code (even though the underlying information is "the same").
Validation that the values that appear in the http request really do satisfy the requirements of the customer schema should happen out near the controller, rather than in near the model. It's the controller, after all, that is responsible for returning client errors if the payload doesn't satisfy the schema (ex: 422 Unprocessable Entity).
Having verified that the input is satisfactory, you can then transform the information (if necessary) from the HTTP representation of the information to the domain model's representation. That should always Just Work[tm] -- if it doesn't it indicates that you have a requirements gap somewhere.
It doesn't particularly matter where this translation happens; but if you were to imagine having multiple different schemas, or different interfaces that accept this information (a command line app, or a queue reading service, or something), then the translation code probably belongs with the interface, rather than with the domain model.
is my overall flow alright or am I heavily missing somewhere?
Your composition choices look suspicious - in particular the fact that the lifetime of the EventBus belongs to Controller::change but the lifetime of the CustomerRepo belongs to ChangeNameHander::handle.
It will become a god class...
Then break it up. See Mauro Servienti's 2019 talk.
Truth is: data models that are just storing copies of information provided by the outside world aren't particularly interesting. The good bits, that really justify the investment of work, are the state machines that decide things based on the information provided by the outside world.
If a state machine doesn't use a piece of information to make decisions, then that information belongs "somewhere else" - either a different state machine, or someplace less complicated like a database or a cache.
I am trying to follow a tutorial for Zend Auth and Zend Acl using 1.11 framework Link here!
I have setup the authentication successfully and am able to use the authentication for the controller::action pairs given in the Acl.php page. Firstly I would like to test two additional parameter on the users table that whether the user account is activated and if the user is banned by administrator before allowing access to the site. How do I implement that in this code.
Secondly I would like to know how to include all actions under one controller to a User authorization level. i.e. I have a masters controller which has numerous actions under it for various tables. Could you tell me how to restrict access to Masters controller all actions to admin role only. Without adding resources and allow resources for each action in Acl.php. Also please tell me if this logic can be extended to allow access over entire modules instead of just the controllers(by one add resource and allow resource)? If yes how?
Firstly I would like to test two additional parameter on the users
table that whether the user account is activated and if the user is
banned by administrator before allowing access to the site.
The tutorial code uses a vanilla version of Zend_Auth_Adapter_DbTable which uses a specific api for authentication. To make Zend_Auth work how you want it to is not very difficult but will require some thought as you'll need to implement Zend_Auth_Adapter_Interface. Sounds worse then it is, you only have to implement the authenticate() method. Here is an example of an auth adapter that can be used in place of Zend_Auth_Adapter_DbTable:
<?php
//some code truncated for length and relevance
class My_Auth_Adapter implements Zend_Auth_Adapter_Interface
{
protected $identity = null;
protected $credential = null;
protected $usersMapper = null;
public function __construct($username, $password, My_Model_Mapper_Abstract $userMapper = null)
{
if (!is_null($userMapper)) {
$this->setMapper($userMapper);
} else {
$this->usersMapper = new Users_Model_Mapper_User();
}
$this->setIdentity($username);
$this->setCredential($password);
}
/**
* #return \Zend_Auth_Result
*/
public function authenticate()
{
// Fetch user information according to username
$user = $this->getUserObject();
if (is_null($user)) {
return new Zend_Auth_Result(
Zend_Auth_Result::FAILURE_IDENTITY_NOT_FOUND,
$this->getIdentity(),
array('Invalid username')
);
}
// check whether or not the hash matches
$check = Password::comparePassword($this->getCredential(), $user->password);
if (!$check) {
return new Zend_Auth_Result(
Zend_Auth_Result::FAILURE_CREDENTIAL_INVALID,
$this->getIdentity(),
array('Incorrect password')
);
}
// Success!
return new Zend_Auth_Result(
Zend_Auth_Result::SUCCESS,
$this->getIdentity(),
array()
);
}
// public function setIdentity($userName)
// public function setCredential($password)
// public function setMapper($mapper)
/**
* #return object
*/
private function getUserObject()
{
return $this->getMapper()->findOneByColumn('username', $this->getIdentity());
}
/**
* #return object
*/
public function getUser()
{
$object = $this->getUserObject();
$array = array(
'id' => $object->id,
'username' => $object->username,
'role' => $object->getRoleId()
);
return (object) $array;
}
// public function getIdentity()
// public function getCredential()
// public function getMapper()
}
You can modify the auth adapter to do pretty much anything you need.
As far as your access list is concerned, the thing to remember is that you resources are defined by a string. In the case of this tutorial a resource is defined as:
$this->add(new Zend_Acl_Resource('error::error'));
where the string on the left side of the colon represents the controller and the string on the right side of the colon represents the action. it's this line in the acl plugin that tell's us what the resources are:
if(!$acl->isAllowed($user->role, $request->getControllerName() . '::' . $request->getActionName()))
you can change this definition of what your resources represent to anything that works for you.
It's very difficult to provide hard and fast rules on how to implement an ACL because it seems that every project needs something different.
Look around the web and you'll find several different implementations of a Zend Framework ACL, some of them can be very complex.
Here is one that might provide some more insight. http://codeutopia.net/blog/2009/02/06/zend_acl-part-1-misconceptions-and-simple-acls/
good luck
I'm creating a service to fetch some user data
class ExampleService{
// ...
public function getValueByUser($user)
{
$result = $this->em->getRepository('SomeBundle:SomeEntity')->getValue($user);
if (!$result instanceof Entity\SomeEntity) {
throw new Exception\InvalidArgumentException("no value found for that user");
}
return $result;
}
}
Then in my controller I have
// ...
$ExampleService = $this->get('example_serivce');
$value = $ExampleService->getValueByUser($user);
Should I be using an exception here to indicate that no value was found for that user in the database?
If I should, how do I handle what is returned from $ExampleService->getValueByUser($user) in the controller - let's say I just want to set a default value if nothing is found (or exception returned)
Here is how I do it. Let's use a user service and a controller as an example. It's not an exceptional condition in the service layer — it just returns the result without checking it:
class UserService
{
public function find($id)
{
return $this->em->getRepository('UserBundle:User')->find($id);
}
}
But in the controllers layer I throw an exception if the requested user not found:
class UserController
{
public function viewAction($id)
{
$user = $this->get('user.service')->find($id);
if (!$user) {
throw $this->createNotFoundException(
$this->get('translator')->trans('user.not_found')
);
}
// ...
}
}
Where you want to handle the exception is kind of up to you, however I would handle it in the controller (and throw it in the model). I usually try to call a different template if there is an error so as to avoid a bunch of conditionals, but sometimes you just have to put extra logic in your template instead.
Also, you have to ask yourself if this is really an exceptional condition - it might be easier to return null and handle that return value in your controller. I can't really tell from the data objects (value, service, and user) whether this is something that will happen all the time or not.
I am writing an CSV/Excel-->MySQL import manager for an MVC application (Kohana/PHP).
I have a controller named "ImportManager" which has an action named "index" (default) which displays in a grid all the valid .csv and .xls files that are in a specific directory and ready for import. The user can then choose the files he wants to import.
However, since .csv files import into one database table and .xls files import into multiple database tables, I needed to handle this abstraction. Hence I created a helper class called SmartImportFile to which I send each file be it .csv or .xls and then I get then ask this "smart" object to add the worksheets from that file (be they one or many) to my collection. Here is my action method in PHP code:
public function action_index()
{
$view = new View('backend/application/importmanager');
$smart_worksheets = array();
$raw_files = glob('/data/import/*.*');
if (count($raw_files) > 0)
{
foreach ($raw_files as $raw_file)
{
$smart_import_file = new Backend_Application_Smartimportfile($raw_file);
$smart_worksheets = $smart_import_file->add_smart_worksheets_to($smart_worksheets);
}
}
$view->set('smart_worksheets', $smart_worksheets);
$this->request->response = $view;
}
The SmartImportFile class looks like this:
class Backend_Application_Smartimportfile
{
protected $file_name;
protected $file_extension;
protected $file_size;
protected $when_file_copied;
protected $file_name_without_extension;
protected $path_info;
protected $current_smart_worksheet = array();
protected $smart_worksheets = array();
public function __construct($file_name)
{
$this->file_name = $file_name;
$this->file_name_without_extension = current(explode('.', basename($this->file_name)));
$this->path_info = pathinfo($this->file_name);
$this->when_file_copied = date('Y-m-d H:i:s', filectime($this->file_name));
$this->file_extension = strtolower($this->path_info['extension']);
$this->file_extension = strtolower(pathinfo($this->file_name, PATHINFO_EXTENSION));
if(in_array($this->file_extension, array('csv','xls','xlsx')))
{
$this->current_smart_worksheet = array();
$this->process_file();
}
}
private function process_file()
{
$this->file_size = filesize($this->file_name);
if(in_array($this->file_extension, array('xls','xlsx')))
{
if($this->file_size < 4000000)
{
$this->process_all_worksheets_of_excel_file();
}
}
else if($this->file_extension == 'csv')
{
$this->process_csv_file();
}
}
private function process_all_worksheets_of_excel_file()
{
$worksheet_names = Import_Driver_Excel::get_worksheet_names_as_array($this->file_name);
if (count($worksheet_names) > 0)
{
foreach ($worksheet_names as $worksheet_name)
{
$this->current_smart_worksheet['name'] = basename($this->file_name).' ('.$worksheet_name.')';
$this->current_smart_worksheet['kind'] = strtoupper($this->file_extension);
$this->current_smart_worksheet['file_size'] = $this->file_size;
$this->current_smart_worksheet['when_file_copied'] = $this->when_file_copied;
$this->current_smart_worksheet['table_name'] = $this->file_name_without_extension.'__'.$worksheet_name;
$this->assign_database_table_fields();
$this->smart_worksheets[] = $this->current_smart_worksheet;
}
}
}
private function process_csv_file()
{
$this->current_smart_worksheet['name'] = basename($this->file_name);
$this->current_smart_worksheet['kind'] = strtoupper($this->file_extension);
$this->current_smart_worksheet['file_size'] = $this->file_size;
$this->current_smart_worksheet['when_file_copied'] = $this->when_file_copied;
$this->current_smart_worksheet['table_name'] = $this->file_name_without_extension;
$this->assign_database_table_fields();
$this->smart_worksheets[] = $this->current_smart_worksheet;
}
private function assign_database_table_fields()
{
$db = Database::instance('import_excel');
$sql = "SHOW TABLE STATUS WHERE name = '".$this->current_smart_worksheet['table_name']."'";
$result = $db->query(Database::SELECT, $sql, FALSE)->as_array();
if(count($result))
{
$when_table_created = $result[0]['Create_time'];
$when_file_copied_as_date = strtotime($this->when_file_copied);
$when_table_created_as_date = strtotime($when_table_created);
if($when_file_copied_as_date > $when_table_created_as_date)
{
$this->current_smart_worksheet['status'] = 'backend.application.import.status.needtoreimport';
}
else
{
$this->current_smart_worksheet['status'] = 'backend.application.import.status.isuptodate';
}
$this->current_smart_worksheet['when_table_created'] = $when_table_created;
}
else
{
$this->current_smart_worksheet['when_table_created'] = 'backend.application.import.status.tabledoesnotexist';
$this->current_smart_worksheet['status'] = 'backend.application.import.status.needtoimport';
}
}
public function add_smart_worksheets_to(Array $smart_worksheets = array())
{
return array_merge($smart_worksheets, $this->get_smart_worksheets());
}
public function get_smart_worksheets()
{
if ( ! is_array($this->smart_worksheets))
{
return array();
}
return $this->smart_worksheets;
}
}
In a code review I was told that it might be better not to have a helper class like this but to keep the bulk of the code in the controller action method itself. The argumentation was that you should be able to look at the code in a controller action and see what it does instead of having it call external helper classes outside of itself. I disagree. My argumentation is:
you should create a helper class anytime it makes code clearer, as in this case, it abstracts away the fact that some files have one worksheet or many worksheets in them, and allows for easy future extension, if, say, we want to also import from sqlite files or even directories with files in them, this class abstraction would be able to handle this nicely.
moving the bulk of the code from this helper class back into the controller would force me to create internal variables in the controller which make sense for this particular action, but may or may not make sense to other action methods within the controller.
if I were programming this in C# I would make this helper class a nested class which would literally be an internal data structure that is inside of and only available to the controller class, but since PHP does not allow nested classes, I need to call a class "outside" the controller to help manage this abstraction in a way that makes the code clear and readable
Based on your experience of programming in the MVC pattern, should the above helper class be refactored back into the controller or not?
There are two approaches to controllers: make it thin or thick. When I started my adventure with MVC I made a mistake of creating thick controllers - now I prefer make it as thin as possible. Your solution is good in my opinion.
Here is how I would redesigned your code even further:
class Backend_Application_SmartImport {
public function __construct( $raw_files ) {
}
public function process() {
foreach ($raw_files as $raw_file) {
// (...)
$oSmartImportFileInstance = $this->getSmartImportFileInstance( $smart_import_file_extension );
}
}
protected function getSmartImportFileInstance( $smart_import_file_extension ) {
switch ( $smart_import_file_extension ) {
case 'xml':
return new Backend_Application_SmartImportFileXml();
// (...)
}
}
}
abstract class Backend_Application_SmartImportFile {
// common methods for importing from xml or cvs
abstract function process();
}
class Backend_Application_SmartImportFileCVS extends Backend_Application_SmartImportFile {
// methods specified for cvs importing
}
class Backend_Application_SmartImportFileXls extends Backend_Application_SmartImportFile {
// methods specified for xls importing
}
The idea is to have two classes responsible for processing xml and cvs inheriting from a base class. The main class uses a special method to detect how the data should be processed (Strategy Pattern). The controller just passed a list of files to the instance of Backend_Application_SmartImport class and passes result of process method to the view.
The advantage of my solution is that code is more decoupled and you can easily and in a clean way add new types of processing like xml, pdf, etc.
I agree with you Edward.
Your ImportController does what a Controller is meant to do. It generates the list of files for the user to view and act on, it then passes that list to the View for it to display. I am presuming that you have a process action or similar which is handles the request when a user has selected a file, this file is then passed on to the Helper in question.
The Helper is a perfect example of abstraction and entirely justified in its usage and existence. It is not coupled with the Controller in anyway and doesn't need to be. The Helper could be easily used in other scenarios where the Controller is not present, for example a CRON task, a public API which users can call programmatically without your ImportController.
Your right on the ball with this one. Stick it to 'em!