Using Eloquent's ORM to insert usermeta.id into posts.usermeta_id - php

I'm working on a user-generated content blog that allows a user to go through the whole upload process before being prompted to sign up. Basic flow: fill out form to pick username/basic info->upload blog post->prompt to sign up with email/password. The purpose of reversing the normal flow is to increase the UX and conversion rate and avoid a wall in the beginning.
Instead of migrating, I've just created the tables manually in PHPmyAdmin. I have 3 relational models: Usermeta->hasOne(App\Mopdels\Post), Post->belongsTo(App\Models\Usermeta), and User->belongsTo(App\Models\Usermeta).
What I'm having trouble with is once the user has created a username and submits the first form to the usermeta table, and then submits the second form to upload their blog post to the post table, it doesn't seem to be attaching the usermeta.id to posts.usermeta_id linking them together. I must be missing something or not attaching it correctly. Here's my StoryController:
<?php
namespace App\Controllers\Story;
use App\Models\Post;
use App\Models\User;
use App\Models\Usermeta;
use App\Controllers\Controller;
use Respect\Validation\Validator as v;
class StoryUploadController extends Controller
{
public function guidance($request, $response)
{
return $this->view->render($response, 'storyupload/guidance.twig');
}
//set up our the Upload Story class so the user can upload their story
//render the view 'uploadstory.twig'
public function getStoryUpload($request, $response)
{
return $this->view->render($response, 'storyupload/upload.twig');
}
// This method is called when the user submits the final form
public function postStoryUpload($request, $response, $id)
{
//set up our validation rules for our complete sign up form
$validation = $this->validator->validate($request, [
'title' => v::stringType()->notEmpty()->length(1, 80),
'body' => v::stringType()->notEmpty()->length(1, 2500),
]);
//if validation fails, stay on story upload page
if ($validation->failed()) {
return $response->withRedirect($this->router>pathFor('storyupload.upload'));
}
$user = Usermeta::find($id)->first();
//We can use our Post Model to send the form data to the database
$post = Post::create([
'title' => $request->getParam('title'),
'body' => $request->getParam('body'),
'category' => $request->getParam('category'),
'files' => $request->getParam('img_path'),
'usermeta_id' => usermeta()->attach($user->id),
]);
//after submit, redirect to completesignup page
return $response->withRedirect($this->router->pathFor('auth.completesignup'));
}
}
I continue to get the error 'usermeta_id cannot be null' so it's definitely not pulling the id from the usermeta table correctly.
I've used the create() method to send the usermeta data to the table in my Auth controller.
Would it be better to have all of my form submissions in the Auth controller and what is the proper way using my example to make sure that my posts.usermeta_id is linked to my usermeta.id?
The usermeta form is taken care of by my Auth Controller:
//render the view 'signup.twig'
public function getSignUp($request, $response)
{
return $this->view->render($response, 'auth/signup.twig');
}
// This method is called when the user submits the form
public function postSignUp($request, $response)
{
$validation = $this->validator->validate($request, [
'name' => v::notEmpty()->alpha(),
'username' => v::noWhitespace()->notEmpty()->UsernameAvailable(),
'city' => v::notEmpty()->alpha(),
'country' => v::notEmpty()->alpha(),
]);
//if validation fails, stay on signup page
if ($validation->failed()) {
return $response->withRedirect($this->router->pathFor('auth.signup'));
}
$usermeta = Usermeta::create([
'name' => $request->getParam('name'),
'username' => $request->getParam('username'),
'city' => $request->getParam('city'),
'country' => $request->getParam('country'),
'share_location' => $request->getParam('share_location'),
]);
//after submit, redirect to storyupload/guidance
return $response->withRedirect($this->router>pathFor('storyupload.guidance'));
}

I wrote quite a bit here. To jump directly to what I believe will solve your problem, see the "Your Issue" section. The rest is here as an educational exercise.
A Quick Intro to Laravel Relations
As you probably already know, "relations" in Laravel are virtual concepts that are derived from the hard data in the database. Because they are virtual, there is some overlap in the definition of relations.
When you say "Usermeta has one Post" - what this means is that the posts table will have a usermeta_id field.
When you say "Post belongs to Usermeta" - what this means is that the posts table will have a usermeta_id field.
Notice that these two relations map to the exact same field in the exact same table. Declaring one relation will declare the other by simple congruence. "Usermeta has one Post" and "Post belongs to Usermeta" are identical relations.
A Tweak to Your Relations
There's one other relation that share this same schema (the posts table have a usermeta_id field). That is "Usermeta has many Posts". The difference here is not in how the relations are stored to the database, but in how Laravel interprets the relations and in what queries Laravel will run.
When you say "Usermeta has one Post", Laravel will scan the database for the first Post with a matching usermeta_id and return that as an instance of the Usermeta model.
When you say "Usermeta has many Posts", Laravel will scan the database for all matching usermeta_ids and return them as a Collection of Usermeta models. You likely want this second behavior -- otherwise users won't be able to make a second post after they sign up.
Setting the usermeta_id Field
Laravel allows you to set database fields directly through a relationship. See their documentation on inserting related models for details.
Because many relationships are just ciphers for the same underlying schema, there's no need to insert or update a related model both ways. For instance, suppose we had the following two models:
class User extends Eloquent {
public function posts() {
return $this->hasMany("App\Post");
}
}
class Post extends Eloquent {
public function user() {
return $this->belongsTo("App\User");
}
}
In this case, the following two lines of code are identical and you only need to use one of them:
$post->user()->associate($user);
$user->posts()->save($post);
Both of these will have the same effect (setting the user_id field on the posts table)
The reason I mention this is that it looks like you're trying to double-dip in your code. You're using attach() (conceivably to set the usermeta_id) and you're also setting the usermeta_id directly. I've added a side-note on the attach method below - as I don't believe it's the right method, anyway.
To use Laravel's relations, you would want code like the following to set this field:
public function postStoryUpload($request, $response, $id)
{
//set up our validation rules for our complete sign up form
$validation = $this->validator->validate($request, [
'title' => v::stringType()->notEmpty()->length(1, 80),
'body' => v::stringType()->notEmpty()->length(1, 2500),
]);
//if validation fails, stay on story upload page
if ($validation->failed()) {
return $response->withRedirect($this->router>pathFor('storyupload.upload'));
}
$user = Usermeta::find($id)->first();
//We can use our Post Model to send the form data to the database
$post = Post::create([
'title' => $request->getParam('title'),
'body' => $request->getParam('body'),
'category' => $request->getParam('category'),
'files' => $request->getParam('img_path'),
]);
// Set the usermeta_id field
$post->usermeta()->associate($user);
// Save the model so we write changes to the database
$post->save();
//after submit, redirect to completesignup page
return $response->withRedirect($this->router->pathFor('auth.completesignup'));
}
Manually Setting the usermeta_id Field
Instead of using Laravel's relations to set this field, you can set the field manually. This can sometimes be cleaner, but it's less explicit and can lead to minor bugs if you aren't careful. To do this, you need to treat the usermeta_id field like any other field on your model.
$post->usermeta_id = $user->id;
This also works when mass assigning attributes using fill or create like so:
$post = \App\Post::create([
'title' => $title,
'body' => $body,
'usermeta_id' => $user->id
]);
$post->fill([
'title' => $title,
'body' => $body,
'usermeta_id' => $user->id
]);
Note that when manually setting the usermeta_id like this, you do not need to use any relationship methods. The following code is redundant:
$post->usermeta_id = $user->id;
$post->usermeta()->associate($user);
Your Issue (I Believe)
There's a caveat to mass assignment, however. Per the Laravel documentation, mass assignment requires you to fill out the model's fillable or guarded attributes.
This is one of the most common bugs, if not the most common bug, in any Laravel code - and it doesn't throw an obvious error so it's easy to miss. Consider the following model:
class Post extends Eloquent {
private $fillable = ["title", "body"];
}
If you attempt to mass assign the usermeta_id field like so:
$post = \App\Post::create([
'title' => $title,
'body' => $body,
'usermeta_id' => $user->id
]);
Then it will silently fail. No error is thrown and the Post is created but the usermeta_id field will be NULL - because it's not mass assignable. This is fixed by updating your model like so:
class Post extends Eloquent {
private $fillable = ["title", "body", "usermeta_id"];
}
I will repeat again, as I did above, that if using mass assignment like this you do not not need to use the associate or save relationship methods. This would be redundant. Therefore you can just set usermeta_id directly to $user->id without any of the usermeta()->associate() shenanigans.
The Bugs I Mentioned
I mentioned that manually setting the field like this can cause bugs. So let's actually discuss what some of those bugs are now instead of glossing over them.
If you update the relationship field manually, Laravel will be unaware that the two models are related until it reloads the model from the database. Consider the following two chunks of code:
$post = new Post();
$post->usermeta_id = $user->id;
dd( $post->usermeta->name );
$post = new Post();
$post->usermeta()->associate($user);
dd( $post->usermeta->name );
The first code block will fail, throwing the error "cannot read attribute of null object" -- because as far as Laravel is aware, $post->usermeta is NULL. You set $post->usermeta_id, but you didn't set $post->usermeta.
The second code block will work as expected, because by running the associate function it sets both usermeta_id and usermeta.
95% of the time this doesn't really cause any issues, however. If you're using an asynchronous API call to save the post and then a separate asynchronous API call to read the post at a later time, then Laravel will read the post from the database and properly set up the relation automatically when we sees the usermeta_id field is filled out.
Side-note On the attach() Method
Laravel uses different methods for saving different types of relations - because the different relations imply different underlying database fields.
associate: This sets the *_id field on the current model's table. For instance: $post->user()->associate($user) will set the user_id on the posts table
save: This sets the *_id field on the other model's table. For instance: $post->comments()->save($comment) will set the post_id on the comments table
attach: This sets both *_id fields on a linking table for many to many relationships. For instance, if you had a tag system then $post->tags()->attach($tag) would set post_id and tag_id on the post_tags table
It can be a bit tricky to remember which of these three functions you need. In general, there's a direct mapping from relation to function:
hasOne, hasMany --> save
belongsTo --> associate
belongsToMany --> attach

Related

Inserting data into laravel db

I know that data can be inserted into the database in
Method 1:
public function store(Request $request)
{
// Validate the request...
$flight = new Flight;
$flight->name = $request->name;
$flight->save();
}
Method 2:
$flight = Flight::create([
'name' => 'London to Paris',
]);
What is the best way to use when inserting 1 value? What is the best way to use when you need to insert, say, 10 values? And are there any other better ways to insert values?
Technically speaking, there isn't much difference, but the main thing about create is that it has mass assignment related issues.
What it means is that, for example if you have a model User which has certain fields including a role field which can be user or admin.
Then if you create a new record using create method like this:
$user = User::create($request->all());
then the member can pass the parameter role in request inputs and change his/her own role and get admin privileges, simple as that! You can prevent this with the property $fillable inside your models, but if it is not taken care of properly it will lead to these kind of issues.
One another point is that, the create method uses the save() itself, if you look at its implementation you can see this:
public function create(array $attributes = [])
{
return tap($this->newModelInstance($attributes), function ($instance) {
$instance->save();
});
}

CakePHP 4 - how to validate forms that need to save data to multiple tables

Apologies if this has been asked before. All of the examples I can find are old or apply to legacy versions of CakePHP, e.g. cakephp: saving to multiple models using one form is 7 years old.
I have an application in CakePHP 4.1.6. Two of the tables in the database are called tbl_users and tbl_orgs ("orgs" in this case means "Organisations").
When I add an Organisation I also want to create a User who is the main contact within the Organisation. This involves saving to both the tbl_orgs and tbl_users tables when the form is submitted.
The problem I'm experiencing is how to get the form working in a way where it will run the validation rules for both tbl_users and tbl_orgs when submitted.
This is how our application is currently structured:
There is a Controller method called add() in src/Controller/TblOrgsController.php. This was generated by bake and was initially used to insert a new Organisation into the tbl_orgs table. At this point it didn't do anything in terms of tbl_users however it worked in terms of saving a new Organisation and running the appropriate validation rules.
One validation rule is that every companyname record in tbl_orgs must be unique. If you try to insert more than 1 company with the name "My Company Limited" it would give the validation error "This company name already exists":
// src/Model/Table/TblOrgsTable.php
public function buildRules(RulesChecker $rules): RulesChecker
{
$rules->add(
$rules->isUnique(['companyname']),
[
'errorField' => 'companyname',
'message' => 'This company name already exists',
]
);
return $rules;
}
Whilst the above applies to TblOrgs we also have an buildRules() in TblUsers which applies similar logic on an email field to make sure that all email addresses are unique per user.
In the add() Controller method we start by specifying a new empty entity for TblOrgs:
// src/Controller/TblOrgsController.php
public function add()
{
$org = $this->TblOrgs->newEmptyEntity();
// ...
$this->set(compact('org'));
}
When the form is created we pass $org:
// templates/TblOrgs/add.php
<?= $this->Form->create($org) ?>
<?= $this->Form->control('companyname') ?>
<?= $this->Form->end() ?>
When the TblOrgs fields are rendered by the browser we can inspect the HTML and see these are obeying the corresponding Model. This is clear because of things such as required="required" and maxlength="100" which correspond to the fact that field is not allowed to be empty and is a VARCHAR(100) field in the database:
<input type="text" name="companyname" required="required" id="companyname" maxlength="100">
It also works in terms of the rules specified in buildRules for TblOrgs. For example if I enter the same company name twice it shows the appropriate error in-line:
I then tried to introduce fields for TblUsers. I prefixed the form fields with dot notation, e.g. this is intended to correspond to tbl_users.email input field:
<?= $this->Form->control('TblUser.email') ?>
When inspecting the HTML it doesn't do the equivalent as for TblOrgs. For example things like maxlength or required are not present. It effectively isn't aware of TblUsers. I understand that $org in my Controller method is specifying a new entity for TblOrgs and not TblUsers. I reviewed the CakePHP documentation on Saving With Associations which says
The save() method is also able to create new records for associations
However, in the documentation the example it gives:
$firstComment = $articlesTable->Comments->newEmptyEntity();
// ...
$tag2 = $articlesTable->Tags->newEmptyEntity();
In this case Tags is a different Model to Comments but newEmtpyEntity() works for both. With this in mind I adapted my add() method to become:
$org = $this->TblOrgs->TblUsers->newEmptyEntity();
But this now gives an Entity for TblUsers. It seems you can have either one or the other, but not both.
The reason this doesn't work for my use-case is that I can either run my Validation Rules for TblOrgs (but not TblUsers) or vice-versa.
How do you set this up in a way where it will run the validation rules for both Models? It doesn't seem to be an unreasonable requirement that a form may need to save data to multiple tables and you'd want the validation rules for each of them to run. I get the impression from the documentation that it is possible, but it's unclear how.
For reference there is an appropriate relationship between the two tables:
// src/Model/Table/TblOrgsTable.php
public function initialize(array $config): void
{
$this->hasMany('TblUsers', [
'foreignKey' => 'o_id',
'joinType' => 'INNER',
]);
}
and
// src/Model/Table/TblUsersTable.php
public function initialize(array $config): void
{
$this->belongsTo('TblOrgs', [
'foreignKey' => 'o_id',
'joinType' => 'INNER',
]);
}
Okay, lots of confusion to clear up here. :-) My assumption here, based on what you've written, is that you're trying to use a single form to add a new organization, and the first user in it, and then maybe later you'll add more users to the org.
First, $this->TblOrgs->TblUsers is your users table object, so when you use
$org = $this->TblOrgs->TblUsers->newEmptyEntity();
what you're doing is creating a new user entity. The fact that you got to that table object through the orgs table, and that you're calling it $org doesn't change that. It doesn't somehow magically create a blank org entity with a blank user entity in it. But you don't need that entity structure here at all here, just the empty org entity. Go back to simply:
$org = $this->TblOrgs->newEmptyEntity();
Now, in your form, you'll want something like this:
<?= $this->Form->create($org) ?>
<?= $this->Form->control('companyname') ?>
<?= $this->Form->control('tbl_users.0.email') ?>
<?= $this->Form->end() ?>
The field is called tbl_users.0.email because:
The table name gets converted to lower case underscore format.
It's a hasMany relation from orgs to users, so it's expecting an array of users; we have to give a numeric index into that array, and 0 is a great place to start. If you were going to add a second user at the same time, the field for that would be tbl_users.1.email.
Note: A great way to figure out what format the form helper is expecting you to create your field names in is to read an existing set of records from the database (in this case, an org and its users), and then just dump that data, with something like debug($org);. You'll see that $org has a property called tbl_users, which is an array, and that will point straight to this structure I've described above.
With the fields set up like this, you should be able to patch the resulting data directly into your $org entity in your controller, and save that without any other work. The patch will created the entire structure, with a entity of class TblOrg, with a tbl_users property which is an array containing a single entity of class TblUser, and validation will have been done on both of them. (At least it should; you can use debug($org); as mentioned above to confirm it.) And when you save this entity, it will first save the TblOrg entity, then add that new ID into the TblUser entity before saving it, as well as checking the rules for both and making sure that nothing gets saved to the database if it can't all be saved. That all happens automatically for you with the single save call!
If your association was a hasOne or belongsTo relation (for example if you were adding a new user and also the org that they're in, instead of the other way around), you could dump a sample $user, and see that it has a property called tbl_org which is just a straight-up entity, not an array of entities, and note that tbl_org is now singular, because it's just one entity instead of a bunch. In this case, the field name to use would be tbl_org.companyname, no array index in there at all.

Laravel: Update single field of table using a Resource Controller

I have a Resource Controller (with all the actions: index, create, store, show, edit, update and destroy) and I was wondering what is the best approach to edit a single field column?
Let's say we have a Users table with name, email, password and active (active is a tiny int 0 or 1).
In the users management page, there is a button to activate/deactivate users (makes a request to the server to update the "active" field for the selected user).
Should I create a new method updateStatus in the Controller or is there a way to handle this using the update method?
I don't want, by mistake, allow empty values in the name, email or password when updating the "active" column, so I need to keep the validation rules (in short, all fields are required), but this means when updating the "active" field, I need to pass all the user data in the request.
At this point I'm very confused and all help will be appreciated.
Thanks in advance!
When you send an instance from edit action to the form , all the data will be sent and you can edit one or more columns if you need .
For instance :
public function update(Request $request , $id) {
$data = YourModel::find($id);
$data->someColumn = $request->someColumn;
$data->save();
}
other fields that you didn't send any value for them will be saved as they were before . for this you can set the form like below :
{!! Form::model($yourInstance,['route'=>['someRoute.update','id'=>$yourInstance->id],'method'=>'PATCH',]) !!}
It sounds like you are new to Laravel, and some key concepts can be hard to grasp.
In my opinion the best way to do it would be via a Model class. This is slightly confused by the fact that Laravel has a built in Users model, so I'm going to use a different model as the example of how to update a db field.
php artisan make:model MyData
Will create a new empty model file for the MyData table in app/
The file will look like this:
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
class MyData extends Model
{
//
}
Even though there's nothing in there, it now allows you do alter the database table using Eloquent.
In your controller add this to make sure the model is included:
use App\MyData as MyData;
The controller should have a method something like this if updating with user input from a form:
public function updateStatus(MyData $myData, Request $request){
$myData->where('id', $request->id)->update(['active' => $request->active]);
}
You could do the exact same thing like this:
public function updateStatus(Request $request){
$data = MyData::find($request->id);
$data->active = $request->active;
$data->save();
}
Both approaches make sense in different circumstances.
See https://laravel.com/docs/5.5/eloquent#updates

Testing existence of Eloquent relationship and creating if it doesn't exist

What would be the best way to create a relationship if it doesn’t exist already, within Eloquent, or at least a central location.
This is my dilemma. A User must have a Customer model relationship. If for whatever reason that customer record doesn’t exist (some bug that stopped it from being created) - I don’t want it to throw errors when I try to retrieve it, but I also request the customer object in multiple locations so I don’t want to test for existence in all those places.
I thought of trying the following in the User model:
public function getCustomerAttribute($value) {
// check $value and create if null
}
But that doesn’t work on relationships, $value is null.
EDIT
I already create a customer upon user creation, but I have come across a situation where it wasn't created and caused exceptions in many places, so I want to fallback.
User::created(function($user) {
$customer = Customer::create([
'user_id' => $user->id
]);
});
Is it possible for you to assume when a user is created that a customer needs to be created as well? If the rest of your system depends on this assumption I would make a model event.
use App\{User, Customer}; // assuming php7.0
UserServiceProvider extends ServiceProvider
{
/**
* Boot
*/
public function boot()
{
// on a side note, we're using "created" not "creating" because the $user->id needs to exist in order to save the relationship.
User::created(function($user) {
$customer = Customer::create([
'user_id' => $user->id
]);
});
}
}

Which cakephp callback methods to choose?

I have table user which have fields username,password, and type. The type can be any or combination of these employee,vendor and client i.e a user can be vendor or client both or some another combination. For type field I have used the multiple checkbox, see the code below. This is the views/users/add.ctp file
Form->create('User');?>
Form->input('username');
echo $this->Form->input('password');
echo $this->Form->input('type', array('type' => 'select', 'multiple' => 'checkbox','options' => array(
'client' => 'Client',
'vendor' => 'Vendor',
'employee' => 'Employee'
)
));
?>
Form->end(__('Submit', true));?>
This is the code I have used in the model file. A callback method beforeSave
app/models/user.php
function beforeSave() {
if(!empty($this->data['User']['type'])) {
$this->data['User']['type'] = join(',', $this->data['User']['type']);
}
return true;
}
This code saves the multiple values as comma separated value in db.
The main problem comes when Im editing a user. If a user has selected multiple types during user creation I can't find the checkbox checked for that user types.
you should never be saving serialized data, json or csv in a field. This makes your life real hard later on down the line.
While habtm is one way to do things, if your binary maths is reasonable you might want to checkout bitmasks for this. here is a great post http://mark-story.com/posts/view/using-bitmasks-to-indicate-status
basics would be
1 = employee
2 = vendor
4 = client
// 8 = next_type
then, if the user was type employee & vendor the type would be 3 (1 + 2) and if it was a vendor & client the type would be 6 (2 + 4)
as you can see there is no way to mix it up, and bitwise works pretty good in mysql aswell so finds are pretty easy. See the post for much more detailed information
You should have a table types and a join table users_types.
What you're looking at is a HABTM relationship, so you should handle it like one.
In the joining UsersType model you should add a custom validation rule that checks if the current combination of types is allowed.
If you want to modify data after it's been found in the database, you can use the afterFind() callback in your model.
So in your case, put something like this is your user model:
function afterFind($results) {
$results['User']['type'] = explode(',', $results['User']['type']);
return $results;
}
There's more info on afterFind in the CakePHP manual.
That being said, it might be worth considering another approach, like a HABTM relationship as deceze first suggested above.

Categories