Laravel 5 - relationships and handling data - model-view-controller

I have a system whereby you can create Documents of different types. Initially, I had a new Model for each Document, but I don't think this is the best way because it can get messy fast. So what I wanted to do is make a generic Documents model, and have individual documents come from this. I have come up with the following type of design
So a Document can have one DocumentA and one DocumentB. DocumentA and DocumentB can only ever be created once per project which is why I have this relationship. Now each form for each document has an upload button, where supporting documents can be uploaded alongside the generated document. So Documents can have one to many FileUploads.
This is where I am confused. A person visits my portal and selects the option to create DocumentA. A form is now displayed to them which looks something like the following
So they enter the data for DocumentA, upload supporting documents, and then click submit.
Now I am thinking about how this can be handled within Laravel.
From what I understand, it will be something like the following
public function store(Request $request, Project $project)
{
$document = new Documents();
$document->documentName = 'Something';
$document->documentA = new DocumentA();
$document->documentA->startDate = Input::get('startDate');
$document->documentA->endDate = Input::get('endDate');
$document->documentA->notes = Input::get('notes');
if (Input::hasFile('filePath')) {
$files = Input::file('filePath');
foreach($files as $file) {
$fileString = "";
$file->move(public_path('uploads'), $file->getClientOriginalName());
$fileString .= public_path('uploads') . '/' . $file->getClientOriginalName();
$document->fileUpload = new FileUploads();
$document->fileUpload->filename = $file->getClientOriginalName();
$document->fileUpload->mime = $file->getClientOriginalExtension();
$document->fileUpload->filepath = $fileString;
$document->fileUpload->documentId = $document->id;
}
}
$document->save();
return Redirect::route('projects.documentA.edit', compact('project', 'documents'));
}
Really, looking for advice as to whether I am designing this correctly, and whether I am handling it correctly within Laravel. I am going to end up with many different Documents, each of them accepting different input.
Any advice appreciated.
Thanks

you want to create different tables for each doc? like you showed above for A and B. It will become messy. Why do not you just manage this in one table by using some identifier col?
If you create one table for documents, one for fileUploads then you would create relationship between them like so
File upload model
<?php
namespace App;
use Illuminate\Database\Eloquent\Model;
class File extends Model
{
/**
* each file belongs to a document
*/
public function document()
{
return $this->belongsTo('App\Document', 'foreign_key', 'primary_key');
}
}
Document model
<?php
namespace App;
use Illuminate\Database\Eloquent\Model;
class Document extends Model
{
/**
* document has multiple files
*/
public function files()
{
return $this->hasMany('App\File', 'foreign_key', 'primary_key');
}
}
Then you can access the records like this
$documents->files()->where('id', 1)->get();
//gives file associated with document id 1

Related

Laravel Model hasMany - getting too may results

I'm learning Laravel, so I'm quite new.
I have 2 Models: House and Translation.
My House Model:
use HasFactory;
protected $guarded = ['id'];
public function trans(){
return $this->hasMany(Translation::class);
}
}
My Translation Model:
function House(){
return $this->belongsTo(House::class);
}
}
When I do in the controller something like ($id=2 e.g) :
$house = House::find($id)::with('trans')->get();
I get a result with all houses (there are currently 2 in the DB).
When I just do the query "House::find($id)->get()" it works fine.
What part am I missing?
::with('trans') returns completely new query and forgets everything about your ::find($id)
you need to use load method.
$house = House::find($id);
$house->load('trans');
What happens is, House::find($id) is a method that returns the element by its primary key.
Then, the ::with('trans') gets the result, sees the House class and starts creating a new query builder for the Model.
Finally, the ->get() runs this new query and the end result is what is return for the $house value.
You have two options that will result to what you want to do.
First one is, to find the house entry and then load its relation
$house = House::find($id);
$house->load('trans');
The second option is this:
$house = House::where('id',$id)->with('trans')->first();
The second one is a query builder that results to the first element with id = $id,
and load its relation with translations.
You can check for more details in the laravel documentation. They have a very well-written documentation and the examples help a lot.

Counting page views with Laravel

I want to implement page view counter in my app. What I've done so far is using this method :
public function showpost($titleslug) {
$post = Post::where('titleslug','=',$titleslug)->firstOrFail();
$viewed = Session::get('viewed_post', []);
if (!in_array($post->id, $viewed)) {
$post->increment('views');
Session::push('viewed_post', $post->id);
}
return view('posts/show', compact('post', $post));
}
I retrieve the popular posts list like this :
$popular_posts = Post::orderBy('views', 'desc')->take(10)->get();
However, I'd like to know if there are any better ways to do this ? And with my current method, can I get a list of most viewed posts in the past 24 hours ? That's all and thanks!
As quoted in # milo526's comment, you can record all hits to your pages in a unique way instead of an increment. With this you have many possibilities to search for access information, including the listing of the posts sorted by most viewed.
Create a table to save your view records:
Schema::create("posts_views", function(Blueprint $table)
{
$table->engine = "InnoDB";
$table->increments("id");
$table->increments("id_post");
$table->string("titleslug");
$table->string("url");
$table->string("session_id");
$table->string("user_id");
$table->string("ip");
$table->string("agent");
$table->timestamps();
});
Then, create the corresponding model:
<?php namespace App\Models;
class PostsViews extends \Eloquent {
protected $table = 'posts_views';
public static function createViewLog($post) {
$postsViews= new PostsViews();
$postsViews->id_post = $post->id;
$postsViews->titleslug = $post->titleslug;
$postsViews->url = \Request::url();
$postsViews->session_id = \Request::getSession()->getId();
$postsViews->user_id = \Auth::user()->id;
$postsViews->ip = \Request::getClientIp();
$postsViews->agent = \Request::header('User-Agent');
$postsViews->save();
}
}
Finally, your method:
public function showpost($titleslug)
{
$post = PostsViews::where('titleslug', '=' ,$titleslug)->firstOrFail();
PostsViews::createViewLog($post);
//Rest of method...
}
To search the most viewed posts in the last 24 hours:
$posts = Posts::join("posts_views", "posts_views.id_post", "=", "posts.id")
->where("created_at", ">=", date("Y-m-d H:i:s", strtotime('-24 hours', time())))
->groupBy("posts.id")
->orderBy(DB::raw('COUNT(posts.id)', 'desc'))
->get(array(DB::raw('COUNT(posts.id) as total_views'), 'posts.*'));
Note that in PostsViews, you have data that can help further filter your listing, such as the session id, in case you do not want to consider hits from the same session.
You may need to adapt some aspects of this solution to your final code.
2020 Update (2)/ With Eloquent Relationships for Laravel 6
If you don't want to add a package to your application. I have developed the following solution based on "Jean Marcos" and "Learner" contribution to the question and my own research.
All credit goes to "Jean Marcos" and "Learner", I felt like I should do the same as Learner and update the code in a way the would be beneficial to others.
First of all, make sure you have a sessions table in the database. Otherwise, follow the steps in Laravel documentations to do so: HTTP Session
Make sure that the sessions are stored in the table. If not, make sure to change the SESSION_DRIVER variable at the .env set to 'database' not 'file' and do composer dump-autoload afterwards.
After that, you are all set to go. You can start by running the following console command:
php artisan make:model PostView -m
This will generate both the model and migration files.
Inside of the migration file put the following Schema. Be cautious with the columns names. For example, my posts table have the "slug" column title name instead of the "titleslug" which was mentioned in the question.
Schema::create('post_views', function (Blueprint $table) {
$table->increments("id");
$table->unsignedInteger("post_id");
$table->string("titleslug");
$table->string("url");
$table->string("session_id");
$table->unsignedInteger('user_id')->nullable();
$table->string("ip");
$table->string("agent");
$table->timestamps();
});
Then put the following code inside the PostView model file.
<?php
namespace App;
use App\Post;
use Illuminate\Database\Eloquent\Model;
class PostView extends Model
{
public function postView()
{
return $this->belongsTo(Post::class);
}
public static function createViewLog($post) {
$postViews= new PostView();
$postViews->post_id = $post->id;
$postViews->slug = $post->slug;
$postViews->url = request()->url();
$postViews->session_id = request()->getSession()->getId();
$postViews->user_id = (auth()->check())?auth()->id():null;
$postViews->ip = request()->ip();
$postViews->agent = request()->header('User-Agent');
$postViews->save();
}
}
Now inside the Post model write the following code. This to create the relation between the posts table and the post_views table.
use App\PostView;
public function postView()
{
return $this->hasMany(PostView::class);
}
In the same Post model you should put the following code. If the user is not logged in the the code will test the IP match. Otherwise, it will test both the session ID and the user ID as each user might have multiple sessions.
public function showPost()
{
if(auth()->id()==null){
return $this->postView()
->where('ip', '=', request()->ip())->exists();
}
return $this->postView()
->where(function($postViewsQuery) { $postViewsQuery
->where('session_id', '=', request()->getSession()->getId())
->orWhere('user_id', '=', (auth()->check()));})->exists();
}
You are ready now to run the migration.
php artisan migrate
When the user ask for the post. The following function should be targeted inside the PostController file:
use App\PostView;
public function show(Post $post)
{
//Some bits from the following query ("category", "user") are made for my own application, but I felt like leaving it for inspiration.
$post = Post::with('category', 'user')->withCount('favorites')->find($post->id);
if($post->showPost()){// this will test if the user viwed the post or not
return $post;
}
$post->increment('views');//I have a separate column for views in the post table. This will increment the views column in the posts table.
PostView::createViewLog($post);
return $post;
}
As I have a separate column for views in the post table. To search the most viewed posts in the last 24 hours you write this code in the controller. Remove paginate if you don't have pagination:
public function mostViwedPosts()
{
return Posts::with('user')->where('created_at','>=', now()->subdays(1))->orderBy('views', 'desc')->latest()->paginate(5);
}
I hope this would help/save someones time.
2020 Update
First of all, thanks a lot "Jean Marcos" for his awesome answer. All credit goes to him, I am just pasting a slightly modified answer combining my knowledge of Laravel as well.
Create a table to save your view records and name it with snake_case plural: post_views
Schema::create("post_views", function(Blueprint $table)
{
$table->engine = "InnoDB";//this is basically optional as you are not using foreign key relationship so you could go with MyISAM as well
$table->increments("id");
//please note to use integer NOT increments as "Jean Marcos' answer" because it will throw error "Incorrect table definition; there can be only one auto column and it must be defined as a key" when running migration.
$table->unsignedInteger("post_id");//note that the Laravel way of defining foreign keys is "table-singular-name_id", so it's preferable to use that
$table->string("titleslug");
$table->string("url");
$table->string("session_id");
$table->unsignedInteger('user_id')->nullable();//here note to make it nullable if your page is accessible publically as well not only by logged in users. Also its more appropriate to have "unsignedInteger" type instead of "string" type as mentioned in Jean Marcos' answer because user_id will save same data as id field of users table which in most cases will be an auto incremented id.
$table->string("ip");
$table->string("agent");
$table->timestamps();
});
Then, create the corresponding model. Please note to create "PascalCase" model name and singular form of the table so it should be like: PostView
<?php
namespace App;
use Illuminate\Database\Eloquent\Model;
class PostView extends Model
{
public static function createViewLog($post) {
$postViews= new PostView();
$postViews->listing_id = $post->id;
$postViews->url = \Request::url();
$postViews->session_id = \Request::getSession()->getId();
$postViews->user_id = (\Auth::check())?\Auth::id():null; //this check will either put the user id or null, no need to use \Auth()->user()->id as we have an inbuild function to get auth id
$postViews->ip = \Request::getClientIp();
$postViews->agent = \Request::header('User-Agent');
$postViews->save();//please note to save it at lease, very important
}
}
Then run the migration to generate this table
php artisan migrate
Finally, your method:
public function showpost($titleslug)
{
$post = PostView::where('titleslug', '=' ,$titleslug)->firstOrFail();
\App\PostView::createViewLog($post);//or add `use App\PostView;` in beginning of the file in order to use only `PostView` here
//Rest of method...
}
To search the most viewed posts in the last 24 hours:
$posts = Posts::join("post_views", "post_views.id_post", "=", "posts.id")
->where("created_at", ">=", date("Y-m-d H:i:s", strtotime('-24 hours', time())))
->groupBy("posts.id")
->orderBy(DB::raw('COUNT(posts.id)'), 'desc')//here its very minute mistake of a paranthesis in Jean Marcos' answer, which results ASC ordering instead of DESC so be careful with this line
->get([DB::raw('COUNT(posts.id) as total_views'), 'posts.*']);
Note that in PostView, you have data that can help further filter your listing, such as the session id, in case you do not want to consider hits from the same session.
You may need to adapt some aspects of this solution to your final code.
So those were few modifications I wanted to point out, also you might want to put an additional column client_internet_ip in which you can store \Request::ip() which can be used as a filter as well if required.
I hope it helps
Eloquent Viewable package can be used for this purpose. It provides more flexible ways to do stuff like this(counting page views).
Note:The Eloquent Viewable package requires PHP 7+ and Laravel 5.5+.
Make Model viewable:
Just add the Viewable trait to the model definition like:
use Illuminate\Database\Eloquent\Model;
use CyrildeWit\EloquentViewable\Viewable;
class Post extends Model
{
use Viewable;
// ...
}
Then in the controller:
public function show(Post $post)
{
$post->addView();
return view('blog.post', compact('post'));
}
After that you can do stuff like this:(see package installation guide for more details)
// Get the total number of views
$post->getViews();
// Get the total number of views since the given date
$post->getViews(Period::since(Carbon::parse('2014-02-23 00:00:00')));
// Get the total number of views between the given date range
$post->getViews(Period::create(Carbon::parse('2014-00-00 00:00:00'), Carbon::parse('2016-00-00 00:00:00')));
// Get the total number of views in the past 6 weeks (from today)
$post->getViews(Period::pastWeeks(6));
// Get the total number of views in the past 2 hours (from now)
$post->getViews(Period::subHours(2));
// Store a new view in the database
$post->addView();
Implements same kind of idea as in the accepted answer, but provides more features and flexibilities.
First of all thanks to user33192 for sharing the eloquent viewable. Just want to make it clearer for others after looking at the docs. Look at the docs to install the package.
Do this in your Post Model:
use Illuminate\Database\Eloquent\Model;
use CyrildeWit\EloquentViewable\InteractsWithViews;
use CyrildeWit\EloquentViewable\Viewable;
class Post extends Model implements Viewable
{
use InteractsWithViews;
// ...
}
In your posts controller, use the record method to save a view;
public function show($slug)
{
$post = Post::where('slug',$slug)->first();
views($post)->record();
return view('posts.show',compact('post'));
}
In your views you can return the views (mine is posts.show) as you want. Check the document for more. I will just the total views of a post.
<button class="btn btn-primary">
{{ views($post)->count() }} <i class="fa fa-eye"></i>
</button>

Using Active Record, how can I save child's detail information through its parent?

I'm using parent->child (master->detail) relation in Yii2 Active Record
When I want to create a child, I have to manually fill its parent info like this:
Relation: Client (1) ---> (n) Comments
class ClientController extends \yii\web\Controller
{
public function actionAddComment() {
$comment = new Comment;
if ($comment->load(Yii::$app->request->post())) {
$comment->client = $this->id; // Client id
$comment->save();
}
return $this->render('view', ['comment'=>$comment]);
}
}
I've optimized it, creating a Comment method to do that:
class Comment extends ActiveRecord {
public function newComment($client) {
$comment = new Comment;
$comment->client = $client; // Client id
return $comment;
}
}
And I have gone through beforeSave in the Comment model, but still not sure if there is a better way.
Is there anything like:
$comment = new Comment(Yii::$app->request->post());
$client->save($comment); // Here the parent is writing his information to the child
Or one-liner shortcut:
$client->save(new Comment(Yii::$app->request->post());
Without having to create this logic in beforeSave?
Yes, I recommend to use the built in link() and unlink() methods provided by Active Record which you can use in your controller to relate or unrelate 2 models either they share many-to-many or one-to-many relationship.
It even has an optional $extraColumns attribute for additional column values to be saved into a junction table if using it link( $name, $model, $extraColumns = [] )
So your code may look like this :
$comment = new Comment;
if ($comment->load(Yii::$app->request->post())) {
$comment->link('client', $this);
}
check docs for more info.
Now about where to use this code to relate models, it depend on how your app is structured. I'm not sure if doing that through a triggered event would be a good practice, you need to remember that errors may happens and
you may need to evaluate certain scenarios or logic before throwing exceptions. So in my case, I prefer to use that code into my Controllers.
Sometimes you need to build a specific action like you did actionAddComment(), In certain other cases like when your Post request is meant to update the Parent model and also update its related child models at once, the Parent's Update Action ClientController::actionUpdate() may be a good place to do so, maybe something like this will do the job :
$params = Yii::$app->request->post();
$client->load($this->params, '');
if ($client->save() === false && !$client->hasErrors()) {
throw new ServerErrorHttpException('Failed to update the object for unknown reason.');
}
foreach ($params["comments"] as $comment) {
// We may be sure that both models exists before linking them.
// In this case I'm retrieving the child model from db so I don't
// have to validate it while i just need its id from the Post Request
$comment = Comment::findOne($comment['id']);
if (!$comment) throw new ServerErrorHttpException('Failed to update due to unknown related objects.');
// according to its documentation, link() method will throw an exception if unable to link the two models.
$comment->link('client', $client);
...

cakephp3.0 query Builder Retrieve data

Hey I am facing some problem I am trying to create a patientcontroller.php in which I want to get data for only one user id from users table so that I can create a dashboard for sngle patient here is my code :
PatientsController.php
<?php
namespace App\Controller;
use App\Controller\AppController;
use Cake\Event\Event;
use Cake\Network\Exception\NotFoundException;
use Cake\ORM\TableRegistry;
class PatientsController extends AppController
{
public function isAuthorized($user)
{
return true;
}
public function index (){
echo $this->Auth->user('id');
$users = TableRegistry::get('Users');
$users->find('all')->where(['id' => 4]);
$this->set('users', $users);
}
I want to get username bio and profile information in my Index.ctp I am trying to pass data using query builder but I am confused so any help
So you have a number of issues with both your approach, techniques and also your code.
Methods
http://book.cakephp.org/3.0/en/controllers.html
If you want to get a single patients record, you should be using the view() method in your controller. So you need to create a new method called view(), in which you can return your record. This method should, in most cases, take an id as the parameter.
Tables
http://book.cakephp.org/3.0/en/orm/table-objects.html
In CakePHP the table which is associated with the controller will be loaded by default. So there is no need to use the TableRegistry to load the table.
Fetching data
http://book.cakephp.org/3.0/en/orm/retrieving-data-and-resultsets.html
When you are looking for a single record you have two choices. You can either just use a find, with a first().
$this->Patients->find()
->where(['id' => $id])
->first();
Or you can use get() which will throw an exception if a record isn't found.
$this->Patients->get($id);
Joining associated data
http://book.cakephp.org/3.0/en/orm/associations.html
If you want to join data to your user, then you'll need Tables to manage that data, which can then be contained, using foreign keys in your database. This will change your finds, to include a contain() call.
$this->Patients->find()
->where(['id' => $id])
->contain(['Profiles])
->first();
Outputting data in the view
You can set the result of your find to the view as you have done, and then you can loop through, or output it how you like, using the variable you've set. In your case $user.
Summary
So overall you can see that there is quite a bit you are missing. Hopefully this will help get you on track.
Given that this is basically your third question here asking for exactly the same thing (getting the results of a query) I will point you to these very useful resources that you should read:
The tutorials: http://book.cakephp.org/3.0/en/tutorials-and-examples.html
They basically have everything you need to understand how queries work and how they can be used in the view and forms.
The ORM manual: http://book.cakephp.org/3.0/en/orm.html
You will find plenty examples of getting data and using it after retrieving it.
I Found A solution for this query : Full code for this kind of problem :
PatientsController.php
<?php
namespace App\Controller;
use App\Controller\AppController;
use Cake\Event\Event;
use Cake\Network\Exception\NotFoundException;
use Cake\ORM\TableRegistry;
class PatientsController extends AppController {
public function isAuthorized($user) {
return true;
}
public function index () {
$id = $this->Auth->user('id');
$articles = TableRegistry::get('Users');
$query = $articles->find()
->where(['id' => $id]);
$this->set(compact('query'));
}
}
For Index.ctp
<!-- File: src/Template/Articles/index.ctp (edit links added) -->
<h1>Patient Dashboard</h1>
I am patient
<?php
foreach ($query as $row) {
echo $row->username ,$row->password;
}
?>
This kind of solution is veryhelpful if you are just trying to move from procedural php to oops in cakephp .

Laravel eager loading result

I'm having trouble with eager loading in laravel
I have these models:
class Page extends Eloquent {
public function translations() {
return $this->has_many('Pagestl', 'page_id');
}
}
and
class Pagestl extends Eloquent {
public static $table = 'pages_tl';
public function page() {
return $this->belongs_to('Page', 'id');
}
}
I want a specific page with its translation data of a specific language.
I retrieve the data like this:
$page_data = Page::with(array('translations' => function($query) {
$query->where('lang_id', '=', 'nl')->first();
}))->find($id);
De result is ok-ish. I get all the page data and the translation of 1 language, dutch (nl). But in order to get a field from the language data I have to this:
$page_data->translations[0]->attributes['content_or_whatever'];
..which i find ugly. I feel i should only have to do something like:
$page_data->translations->content;
..but that gives me an error (Trying to get property of non-object).
Am I missing something or is this just the way it is?
I don't find it necessary to do eager-loading here.
Both ways of doing it will result in 2 queries: select the Page with the given id and then find its first translation.
So, I would do:
$page = Page::find($id);
and then
$page_data = Pagestl::where_page_id_and_lang_id($page->id, 'nl')->first();
Then, assuming the content field exists in pages_tl table, we'll get it by doing:
echo $page_data->content;
Finally, your code looks alright; you don't have to have the second parameter when defining the relationships unless they are different from what Laravel expects.
I'll let you find where the problem is with your code (I couldn't figure out from the information given - could be something to do with strings.php)

Resources