Allow One setAttribute() to Prevent Another One - laravel

I have an entity which I've created two setAttribute functions for:
public function setStartAttribute($value) { }
and
public function setEndAttribute($value) { }
These attributes, start and end, are both datetimes which I check against some criteria in each of my setter function before allowing. Under certain conditions, I prevent or allow the start or end attributes to be updated.
I've hit a wall, however, in that if I prevent one of these from being updated, I need to prevent both. In other words, if the user tries to update the entity with a start date which is out of bounds, I need to prevent the start date from being updated, but I also need to prevent the end date from being update.
As these are two separate functions, I'm not sure how to use one to prevent the other in a case like this.
EDIT:
Since the answer is extremely obvious (just do it both in one function) without adding this extra info, I'll add that the part that makes this less straightforward is that I'm using Backpack for Laravel. Within the Backpack admin panel is the CRUD that lets me create or update my entity. I'm using the date_range field type to allow setting the start and end time/dates on my entity. It's upon saving this that I need to be able to pass both the start and end values to a function and validate them, prior to setting them on my entity. I found that creating the two separate functions above setStartAttribute() and setEndAttribute() allowed me to validate those values and choose whether to assign them to the entity, however I need to be able to use one unified function rather than two separate ones. It is this integration with Backpack which makes this problem less straightforward for me.

If those start and end attributes are connected somehow (one can't be set if another is invalid), you better make one method to set both of them. Something like this:
public function setStartAndEnd($start, $end)
{
if ($start is valid && $end is valid)
{
$this->start = $start;
$this->end = $end;
}
}
Which you can use as follows:
$entity->setStartAndEnd($date, $another_date);

Related

Laravel Nova creating a model with an action in newModel()

I'm in Laravel 8 with Nova 3.22. I have a table that contains a field that is populated automatically with a serial number inside a DB transaction, and in order to enforce that I have a CreateProduct action defined that wraps the creation of that resource. Everywhere I need to create a new instance of that model, I call the action, and I need to do the same in Nova. I've found the newModel() method to override, but it has two problems.
public static function newModel()
{
$instance = new CreateProduct(
new \App\Models\Sku(), //Placeholder
\App\Models\Product::STATUS_DEFAULT,
null
);
return $instance->handle();
}
Firstly, this method is called on create (displaying the input form) as well as store store operations. This means I end up with two calls to my action, creating spurious serial numbers in my DB. If I don't override newModel like this, it creates records that lack a serial number altogether. I have a choice of 0 or 2 calls, but I only want 1!
The second issue is related; when the form is displayed, it calls newModel, but I have to use placeholder data for required params of the action, as those fields have (obviously) not been set yet, but I need to replace those placeholders with the real submitted values the second time, and I'm not sure how I would do that.
I feel I must be missing something – is there something like newModel, but that is only called for a store operation?

Two models, two fields, return preferred if present

Been struggling with how to do this the most optimized way possible...
I have two models: Catalog and Application.
Catalog has a field called name.
Application has a field called name.
Both have a relationship with each other.
I am struggling to find a way to create a function i could use across my Laravel application which i would pass application.id to it and it would return a $app->name value based on the following logic:
if $application->name exists, use this value as the $app->name for the $application object
otherwise, get the $catalog->name value and use it as the $app->name
Note that I would like to create a component #application() where i can simply pass the $application->id and build the display logic (theming/styling) into it.
Since i display this $app->name in many places, i would like to make it as lightweight as possible to avoid unnecessary queries.
I hope this makes sense! There are probably so many ways to go with it, i am lost at figuring out the way way to do this :(
I'm not completely sure to understand your model/DB design, but you could use a custom Helper to use that function through the whole app.
For that, you can create a simple PHP class Helper.php file in app/Http/Helpers folder or whatever location you want. Something like:
<?php
use App\Catalog;
use App\Application;
if (! function_exists('getAppName')) {
function getAppName($id){
// Do your logic here to return the name
$catalog = Catalog::find($id);
return $catalog->name;
}
}
?>
Then in any controller or view, you just do
getAppName($application->id)
Do no forget to add your helpers file to the composer autoload. So in composer.json in Laravel's root folder, add the helper path to the autoload array:
"files": [
"app/Http/Helpers/helpers.php"
],
Last but not least, run the following command:
composer dump-autoload
Please note that function logic is just for sample purposes since I don't know your model structure.
In my opinion, I care about the database cost.
Use ternary expression will be elegant. But it took two times IO costs from database if application name is empty.
$app_name = Application::find($id)->name;
$app_name = empty($app_name) ? Catalog::where('application_id', $id)->first()->name;
And this will more complicated, but the catalog_query only execute when application.name is empty, it execute in database and the result is taken out only once;
And Database will only find the name from one table or two table.
Something like this:
$catalog_query = Catalog::where('catalogs.application_id', $id)->select('catalogs.name')->groupBy('catalogs.name');
// if catalogs and applications relationship is 1:1, use ->limit(1) or remove groupBy('name') is better.
Application::where("applications.id", $id)
->selectRaw("IF(application.name IS NULL OR application.name = '', (" . $catalog_query->toSql() ."), applications.name ) AS app_name")
->mergeBindings($catalog_query->getQuery())
->first()
->app_name;
Hope this will help you.

Form and availability of "formers"

I am stuck on a problem that I did not think before.
I have a form formers with 3 fields (name, firstname, status).
Then, my second form is the trainings with 2 fields (date_sitting, fk_former).
Here, I have 1 sitting date -> 16/07/2019 and the former is Dujardin Luc.
Now my problem is that if I want to create a new sitting for on 18/07/2019.
How to do for that the former Dujardin Luc to be available in my dropdownlist for on 18/07/2019 or another date??
Is it, in my case, I must to create 2 files create.blade ?
The first will just have a field -> date_sitting
And the second form will have the field -> fk_former ?
Here is my TrainingController for information
public function index()
{
$trainings = Training::oldest()->paginate(5);
$formersNoTrainingToday = Training::whereDate('date_sitting', "!=", Carbon::today())
->orWhere('date_sitting', null)->get();
$formers = Former::doesntHave('trainings')->get();
return view('admin.trainings.index', compact('trainings'))
->with('i', (request()->input('page',1) -1)*5);
}
I can't give you the whole answer, as there are too many things to update and a few things to decide.
First, you'll need to change your index() method to both send more and different data. If a former can train on multiple days, the $formers = Former::doesntHave('trainings')->get(); is no longer valid -- you want all formers now so that you can add a new date. So just $formers = Former::get(); now.
Next, your $formersNoTrainingToday is now obsolete, as you don't want to identify those who are unavailable just today, you want a list of all training dates so compare against whatever date the user is looking for in the form. Example: If Dujardin Luc wants training on 18/07/20XX, we don't want him in a list of NoTraining because we want him available, even if he is unavailable today.
Last on the controller side, depending on how you want to do the blade side, you might want to include a variable for an AJAX date to use to send through those formers who are available on the specific date requested by the user through AJAX. For example, if you go through AJAX, you might have a method to just send those available on the date requested like this in your controller (Not exact code):
public function getFormersWithoutTrainingOnDateRequested($dateRequested){
$formersAvailableToday = Training::whereDate('date_sitting', "!=", Carbon::createFromFormat('Y-m-d', $dateRequested))
->orWhere('date_sitting', null)->get();
return json_encode($formersAvailableToday);
}
The next part is on the blade side. You have quite a few choices here, but both of the ones I recommend involve some type of JS or Jquery. In order to populate the list of formers for the day that the user requests, for best user experience, you should get them the list quickly.
Fastest would be to provide a list of trainings (date_sitting, fk_former) to the page from the index. This would be stored as a data-object on some element of the blade page. Then, when the user clicks to change the date, a JS or Jquery function would pull the list into a var, compare it against another list of former's ids stored the same way, and re-write the select list immediately, using only those formers whose ids were not matched for the date.
Second way to do this would be a little slower, but a little easier. It would involve going back to the server to get the list of formers available. So you would have an AJAX function on the same select that goes back to the server to pull those formers available on the date chosen. The success() method of the AJAX pull would provide the info to re-write the select box, or perhaps even re-make the whole form.
Sorry I can't write all the code, you'll have to fill in the blanks where you can, but this is hopefully enough to help you figure out where to go.

How to retrieve a customised list of records according to Repository pattern?

I wanna move the business logic out of controller actions. I read a lot about repository pattern in laravel with tons of examples.
However they're usually pretty straightforward - we have a class that uses some repository to fetch a list of all possible records, the data is returned to the controller and passed to the view.
Now what if our list isn't all the possible records? What if it depends on many things. For example:
we display the list as "pages" so we might need X records for Y-th page
we might need to filter the list or even apply multiple filters (status, author, date from - to etc)
the user can change the sorting of the data (for example by clicking the table column titles)
we might need some data from other data sources (joined tables) or it might even be used for sorting (so lazy loading won't work)
Should I write a special method with all these cases in mind? Something like that:
public function getForDisplay(
$with = array(),
$filters = array(),
$count = 20,
$page = 0,
$orderBy = 'date',
$orderDir = 'DESC'
)
{
//all the code goes here
return $result;
}
And then call it like this from my controller:
$orders = $this->orders->getForDisplay(
array('customer', 'address', 'seller'),
Input::get('filters', array()),
20,
Input::get('page', 0),
Input::get('sort', 'date'),
Input::get('direction', 'DESC')
);
This looks wrong already and we didn't even get to the repositories yet.
What are the best/correct practices for solving situations like this? I'm pretty sure there has to be a way to achieve the desired results without adding all the possible combinations as a method arguments.
Use the repository pattern just for business model updates and you'll end up with very specific query methods (the Domain usually doesn't need many queries and they are pretty straightforward). For UI/reporting querying purposes, you can use a simple DAO/Service/ORM/QUery Handler , that will take some input and returns the desired data (at least part of the view model).
Since you're already using an ORM, you can use it directly. Note that you can use the ORM for domain updates also, but inside a repository's implementation i.e the app only sees the repository interface. We care about separation at the business layer, for UI querying you can skip the unneeded abstraction.
Btw, because we're talking about design, everything is subjective and thus, there's no single best/optimum way of doing things.

CakePHP, organize site structure around groups

So, I'm not quite sure how I should structure this in CakePHP to work correctly in the proper MVC form.
Let's, for argument sake, say I have the following data structure which are related in various ways:
Team
Task
Equipment
This is generally how sites are and is quite easy to structure and make in Cake. For example, I would have the a model, controller and view for each item set.
My problem (and I'm sure countless others have had it and already solved it) is that I have a level above the item sets. So, for example:
Department
Team
Task
Equipment
Department
Team
Task
Equipment
Department
Team
Task
Equipment
In my site, I need the ability for someone to view the site at an individual group level as well as move to view it all together (ie, ignore the groups).
So, I have models, views and controls for Depart, Team, Task and Equipment.
How do I structure my site so that from the Department view, someone can select a Department then move around the site to the different views for Team/Task/Equipment showing only those that belong to that particular Department.
In this same format, is there a way to also move around ignoring the department associations?
Hopefully the following example URLs clarifies anything that was unclear:
// View items while disregarding which group-set record they belong to
http://www.example.com/Team/action/id
http://www.example.com/Task/action/id
http://www.example.com/Equipment/action/id
http://www.example.com/Departments
// View items as if only those associated with the selected group-set record exist
http://www.example.com/Department/HR/Team/action/id
http://www.example.com/Department/HR/Task/action/id
http://www.example.com/Department/HR/Equipment/action/id
Can I get the controllers to function in this manner? Is there someone to read so I can figure this out?
Thanks to those that read all this :)
I think I know what you're trying to do. Correct me if I'm wrong:
I built a project manager for myself in which I wanted the URLs to be more logical, so instead of using something like
http://domain.com/project/milestones/add/MyProjectName I could use
http://domain.com/project/MyProjectName/milestones/add
I added a custom route to the end (!important) of my routes so that it catches anything that's not already a route and treats it as a "variable route".
Router::connect('/project/:project/:controller/:action/*', array(), array('project' => '[a-zA-Z0-9\-]+'));
Whatever route you put means that you can't already (or ever) have a controller by that name, for that reason I consider it a good practice to use a singular word instead of a plural. (I have a Projects Controller, so I use "project" to avoid conflicting with it.)
Now, to access the :project parameter anywhere in my app, I use this function in my AppController:
function __currentProject(){
// Finding the current Project's Info
if(isset($this->params['project'])){
App::import('Model', 'Project');
$projectNames = new Project;
$projectNames->contain();
$projectInfo = $projectNames->find('first', array('conditions' => array('Project.slug' => $this->params['project'])));
$project_id = $projectInfo['Project']['id'];
$this->set('project_name_for_layout', $projectInfo['Project']['name']);
return $project_id;
}
}
And I utilize it in my other controllers:
function overview(){
$this->layout = 'project';
// Getting currentProject id from App Controller
$project_id = parent::__currentProject();
// Finding out what time it is and performing queries based on time.
$nowStamp = time();
$nowDate = date('Y-m-d H:i:s' , $nowStamp);
$twoWeeksFromNow = $nowDate + 1209600;
$lateMilestones = $this->Project->Milestone->find('all', array('conditions'=>array('Milestone.project_id' => $project_id, 'Milestone.complete'=> 0, 'Milestone.duedate <'=> $nowDate)));
$this->set(compact('lateMilestones'));
$currentProject = $this->Project->find('all', array('conditions'=>array('Project.slug' => $this->params['project'])));
$this->set(compact('currentProject'));
}
For your project you can try using a route like this at the end of your routes.php file:
Router::connect('/:groupname/:controller/:action/*', array(), array('groupname' => '[a-zA-Z0-9\-]+'));
// Notice I removed "/project" from the beginning. If you put the :groupname first, as I've done in the last example, then you only have one option for these custom url routes.
Then modify the other code to your needs.
If this is a public site, you may want to consider using named variables. This will allow you to define the group on the URL still, but without additional functionality requirements.
http://example.com/team/group:hr
http://example.com/team/action/group:hr/other:var
It may require custom routes too... but it should do the job.
http://book.cakephp.org/view/541/Named-parameters
http://book.cakephp.org/view/542/Defining-Routes
SESSIONS
Since web is stateless, you will need to use sessions (or cookies). The question you will need to ask yourself is how to reflect the selection (or not) of a specific department. It could be as simple as putting a drop down selection in the upper right that reflects ALL, HR, Sales, etc. When the drop down changes, it will set (or clear) the Group session variable.
As for the functionality in the controllers, you just check for the Session. If it is there, you limit the data by the select group. So you would use the same URLs, but the controller or model would manage how the data gets displayed.
// for all functionality use:
http://www.example.com/Team/action/id
http://www.example.com/Task/action/id
http://www.example.com/Equipment/action/id
You don't change the URL to accommodate for the functionality. That would be like using a different URL for every USER wanting to see their ADDRESS, PHONE NUMBER, or BILLING INFO. Where USER would be the group and ADDRESS, PHONE NUMBER< and BILLING INFO would be the item sets.
WITHOUT SESSIONS
The other option would be to put the Group filter on each page. So for example on Team/index view you would have a group drop down to filter the data. It would accomplish the same thing without having to set and clear session variables.
The conclusion is and the key thing to remember is that the functionality does not change nor does the URLs. The only thing that changes is that you will be working with filtered data sets.
Does that make sense?

Resources