MVC Putting an action in the most appropriate correct controller - model-view-controller

I was just wondering what the best practice approach is for deciding where to create an action/view in certain situations.
If User hasMany Video
where is the best place to create the action/view to show user videos?
So within the Users account page 'My Videos' link do you
just create a users/my_videos action and view.
create videos/my_videos action and view.
or as is most likely you would already have a Controller/Action of videos/index which would have search functionality. Simply use this passing in a user id.
Any thoughts/advice greatly appreciated
Thanks
Leo

One potential option is to do the following:
Since the videos likely have much more code around them than a simple which user has which videos lookup the video list action should be in the VideosController.
In past projects I have (in CakePHP 1.3) used prefix routing to address some of this.
In config/core.php make sure you enable routing.prefixes to include a 'user' prefix.
<?php
... in routes.php ...
Routing.prefixes = array( 'user' );
?>
In the videos controller make an action with the following signature:
<?php
...
public function user_index( $userID = null ){
...
}
?>
and in the views where you link to the list of users videos the html::link call should look similar to the following:
<?php
...
echo $this->Html->link( 'User\'s Videos', array(
'controller' => 'videos',
'action' => 'index',
'prefix' => 'user',
$this->Session->read( 'Auth.User.id' )
));
?>
Of course this assumes you are using the Auth component here to track the logged in user. The Session helper code to read the authenticated user id might need tweaking.
This lets you a) Not worry too much about routing aside from enabling prefix routing and b) will quickly let you have pretty links like so -- site.com/user/videos/index/419
Couple this with some Slug love ( this is the best link for this I have seen - no slug field required on the db layer - http://42pixels.com/blog/slugs-ugly-bugs-pretty-urls )
You could even end up with urls like so quite easily: site.com/user/videos/index/eben-roux
and with just a tiny bit of editing to app/config/routes.php you could eliminate the /index/ portion and the results would be SEO friendly and user friendly in the format:
site.com/user/videos/eben-roux
http://book.cakephp.org/view/945/Routes-Configuration

As always with code you have the two extremes of:
1) Putting everything in a single controller
2) Having every action in a separate controller
The ideal approach will nearly always be somewhere between the two so how to decide what is grouped together and what is separated?
In MVC I tend to look at the Views and see what the commonalities are: as you point out Users have a ref to a collection of Videos in the Model, but would you want both sets of Data in any single View? i.e. In this example is it likely that you would be on a page that both managed user details, and displayed the list of vids? If not then I'd suggest separate controllers.
If either controller would then be extremely simple - e.g. one method, then may be worth considering merging the two.

I like to keeps things separate.
What I'd do is an index action in videos controller, passing user's id as argument and then displaying only current users video.
public function index($id = null){
$this->paginate = array( 'conditions'=> array('Video.user_id' => $id));
$this->set('videos', $this->paginate());
}

My take is that it depends on the responsibility you assign to the controllers.
I would say that something like a User or a Video controller should be concerned with only those entities.
You may want to consider something like a UserDashboard (or something similar but appropriately named) as alluded to by Dunhamzzz in the comments. This can aggegate all the functionality from an "entry" point-of-view. The same way a banner / shortcut / action menu would work.
Your UserDashboard would use whatever data layer / repository is required to get the relevant data (such as the IVideoRepository or IVideoQuery implementation).
Usually when something doesn't feel right it isn't. Try splitting it out and see how it works. You can alsways re-arrange / refactor again later.
Just a thought.

I don't think there's a 'one-rule-fits-all' solution to this question, but I would try to take an approach in which you would determine what the main object is that you're dealing with, and adding the action/view to that object's controller.
In your example I'd say that your main object is a video and that the action you're requiring is a list of video's filtered by a specific property (in this case the user's id, but this could very well be a category, a location, etc.).
One thing I would not do is let your desired URL determine in which controller you put your functionality. URLs are trivially changed with routes.

Related

Is there a one time link generation Laravel?

Is it possible to create a one time link in Laravel? Once you open the link it expires?
I have created a Temporary Signed Link, but I can open it multiple times. How do I counter it?
There is this package that can help you
https://github.com/linkeys-app/signed-url/
This will generate a link valid for 24hours and for just one click .
$link = \Linkeys\UrlSigner\Facade\UrlSigner::generate('https://www.example.com/invitation', ['foo' => 'bar'], '+24 hours', 1);
The first time the link is clicked, the route will work like normal. The second time, since the link only has a single click, an exception will be thrown. Of course, passing null instead of '+24 hours' to the expiry parameter will create links of an indefinite lifetime.
There maybe a package that provides a functionality like this... always worth looking on Packagelist before building something rather generic like this from scratch. But, it's also not a hard one to build from scratch.
First you'll need database persistence, so create a model and a migration called UniqueLink. In the migration you should include a string field called "slug", a string field called path, and a timestamp field called "used_at."
Next create a controller with a single __invoke(string $slug) method. In the method look up the $link = UniqueLink::where('slug', $slug)->first(); Update the models' used_at parameter like so $link->update(['used_at' => Carbon::now()]);
Then return a redirect()->to($link->path);
Add a route to your routes file like this Route::get('/unique-link/{slug}', UniqueLinkController::class);
Now you'll just need to create a method to add these links to the db which create a slug (you could use a UUID from Str::uuid() or come up with something more custom) and a path that the link should take someone. Over all a pretty straight forward functionality.
You could track when the URL is visited at least once and mark it as such for the user if you really want to, or you could reduce the expiry down to a few mins.
URL::temporarySignedRoute( 'foobar', now()->addMinutes(2), ['user' => 100] );

Laravel create related Model view, pass prefilled value

i am implementing a backoffice system with many Models and Relations.
Now im stuck with my UI Stragegy:
Lets assume i have Houses and Rooms. One House has many Rooms.
I have created controllers for both Models the "Laravel" way.(Resource Controllers)
So i have routes for both of them
example.com/backoffice/house
example.com/backoffice/room
What i want to implement sounds simple:
I want an Button inside the Detail View of a House ("Create Room for this House") which redirects me to "room/create" but in the create view i want to set the value for "house_id" to the id of the House i am comming from. So i can normaly use the store method in the RoomController and then redirect back to the house.
I want a general way because i must use this function on many Models/Views. I am thinking about a session variable but i think eventually has someone a better way for generally handling such cases? Or a better idea for UI Handling?
Apparently, Laravel removed some of their awesome documentation for version 5.6, being nested resource controllers.
What you could do, is use nested routes.
Let's assume your current controllers are set up the following way:
Route::resource('houses', 'HouseController');
Route::resource('rooms', 'RoomController');
If you change this part to the following:
Route::resource('houses', 'HouseController');
Route::resource('houses.rooms', 'RoomController');
This couples every room to a house and is really easy to manage. It gives you URL's like houses/4/rooms/create, which gives you a house_id in your create method instantly:
public function create($houseId)
{
return view('houses.rooms.create', ['houseId' => $houseId]);
}
If you want to edit a room, it is exactly the same:
public function edit($houseId, $roomId)
The Laravel 5.1 documentation still has an example of this technique.
To do this, i would suggest the following way (there might be other ways also)
Change Route:
Route::get('room/create/{house_id?},'Controller#action')->name('room.create')
Add <a> tag in house_view.blade.php file.
Create Room for this House
Room Controller file.
public function formCreate($house_id)
{
return view('form.room_create', ['house_id' => $house_id]);
}
Add type hidden <input> tag in room_create.blade.php file
<input type="hidden" id="house_id" name="house_id" value="{{$house_id or ''}}">

Route and view naming conventions

I am looking for some input regarding the naming conventions I use for route names and view directory structures.
Say I have the following routes:
Route::get('/teams/choose', 'ChooseTeamController#index')->name('teams.choose.index');
Route::post('/teams/choose', 'ChooseTeamController#choose')->name('teams.choose');
Route::get('/teams/{team}/manage', 'ManageTeamController#index')->name('teams.team.manage.index');
For the get routes, I would nornally put the views in a directory structure matching the route name. E.g. resources/views/teams/team/manage/index.blade.php. However, I feel that this is way too verbose.
I feel that it would be confusing all round (to myself and other developers) if I was to use a view directory structure like so, rather than the last example: resources/views/team/manage/index.blade.php- the plural of team is not used, so when I have other views, like so (using the original examples convention): resources/views/teams/choose.index they dont visually have the relationship intended. I.e. they have a differing 'root' directory- teams vs team.
Any input or advice would be appreciated.
For the get routes, I would normally put the views in a directory structure matching the route name. E.g. resources/views/teams/team/manage/index.blade.php. However, I feel that this is way too verbose.
I agree.
From the Laravel docs:
Laravel uses the typical RESTful "CRUD" approach when assigning resource routes to a controller. Each verb (i.e. GET, POST, PUT, DELETE) gets a designated URI, an action (technically, a controller method) and a route-name (sometimes, /path/to/blade/view).
So, from your snippet:
// return view(teams.index)
Route::get('/teams', 'TeamController#index');
// return view(teams.create)
Route::get('/teams/create', 'TeamsController#create');
// redirect('/home');
Route::post('/teams', 'TeamController#store');
// return view('teams.profile')
Route::get('/teams/profile', 'TeamController#profile')->name('profile');
I use this resource table to remind me what-to-do and what-not-do all the time.
Perhaps, inspecting some of the awesome Laravel codebases, might help. Plus, a perspective on how other teams are doing things is always priceless.
I found these to be very helpful:
RESTful API Best Practices and Common Pitfalls
RESTful API Design - A Cookbook
Update
The key is to stick to the standard CRUD actions i.e. index, show, create, store, edit, update and delete. The views will fall, right into their place.
Check out Adam Wathan's talk at Laracon EU as he demonstrates how, anything can be CRUDDY with a little imagination.
There are so many ways to maintain routes based on the requirement but i always follow below guidelines which helps me to maintain the file structure and easy to understand.
//listing
Route::get('/teams', 'TeamController#index');
//Create
Route::get('/teams/create', 'TeamController#create');
//Store
Route::post('/teams/store', 'TeamController#store');
//Show
Route::get('/teams/{id}', 'TeamController#show');
//Edit
Route::get('/teams/{id}/edit', 'TeamController#edit');
//Update
Route::put('/teams/{id}/update', 'TeamController#update');
//Delete
Route::delete('/teams/{id}/delete', 'TeamController#delete');
for more information related to proper naming convention you may follow below link
https://laravel.com/docs/7.x/controllers#restful-nested-resources
if you are building with consuming by api as a concern, you wouldn't need the create and edit forms so endpoints can be reduced to:
//listing
Route::get('/teams', 'TeamController#index');
//Store
Route::post('/teams', 'TeamController#store');
//Show
Route::get('/teams/{id}', 'TeamController#show');
//Update
Route::put('/teams/{id}', 'TeamController#update');
//Delete
Route::delete('/teams/{id}', 'TeamController#delete');

Basset 4 (pre-beta)

I'm using Basset 4 to manage assets.
In the config file i'm declaring a collection 'admin'
return array(
'collections' => array(
'admin' => function($collection)
{
$collection->directory('assets/js', function($collection)
{
$collection->add('vendor/jquery-1.9.1.min.js');
});
},
),
...
)
later in a view, I would like to add an extra file in admin collection.
I've try the following code, but it doesn't work:
Basset::collection('admin', function($collection)
{
$collection->add('function.js');
});
Is there a way to add file into a collection from a view or from a controller?
Thank you
Basset isn't really designed to work like that. You should be defining all your assets within that initial call, even though the ability to add assets throughout the execution of an application is possible, it's not recommended.
When building a collection assets that are added for a particular route won't be available to the builder since Artisan doesn't fire any routes, etc.
Adjusting a collection in numerous places can often lead to confusion further down the line.
I know this isn't ideal as you're probably looking at implementing page specific JavaScript, correct? I've thought about it but can't really think of a clean solution (suggestions?), although I've heard of people assigning a unique ID to the body or perhaps some classes that their JavaScript can then attach themselves to.
It's not brilliant but that's the best I can give you at the moment.

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