Unable to insert data in CodeIgniter 4 - php

I am trying out CodeIgniter 4 and I have a problem inserting data.
In my model I only defined the table name, no methods, no nothing. I can display all data in my table but when it comes to inserting data something goes wrong.
My controller code looks like:
$data = [
'sum' => '23',
'type' => 'in',
'name' => 'asd'
];
$expense = new ExpensesModel();
try {
var_dump($expense->insert($data));
} catch (\ReflectionException $e) {
var_dump($e);
}
When I call this endpoint using Postman I get a 500 internal server error.
If I die('asd') before the try-catch I can see the message so this makes me think something happens during the insert method call.
How can I debug this ?

If all you defined within the model was the table name, then you never defined $allowedFields (https://codeigniter.com/user_guide/models/model.html):
This array should be updated with the field names that can be set
during save, insert, or update methods.
You should probably read through that documentation. Moreover, you would troubleshoot these issues by looking at the logs within your project's writable/logs directory, although in this case CI would not consider this a failure as it was you not defining what's allowed into the DB.

Related

Testing insert methods by using factory make of Laravel with a PostgreSQL reserved id

Our Laravel app uses PostgreSQL and before we insert a register in a table we reserve the next valid id by calling PostgreSQL nextval function.
I want to test the insert method of UserRepository. As our database expects to receive an id I would need to generate it also in the test method that tests the insert method, so I use Laravel's factories make method like this to create a fake id:
$userModel = UserModel::factory()->make([
'id' => $this->userRepository->nextValId()
// Another solution 'id' => $this->faker->unique()->randomNumber(5)
]);
$this->userRepository->insert($userModel->toArray());
This might not seem too complex, but imagine I now have an insertMany method that inserts several users at once, and I want to test this insertMany method. In this case the creation with Laravel's factory() gets more complicated. To start off, this code wouldn't work now as the ids of all users would be the same:
$collection = UserModel::factory(4)->make([
'id' => $this->userRepository->nextValId()
// Another solution 'id' => $this->faker->unique()->randomNumber(5)
]);
$this->userRepository->insertMany($collection->toArray());
So I have to rewrite it like this:
$collection = UserModel::factory(4)->make();
$users = $collection->toArray();
$users[0]['id'] = $this->userRepository->nextValId();
$users[1]['id'] = $this->userRepository->nextValId();
$users[2]['id'] = $this->userRepository->nextValId();
$users[3]['id'] = $this->userRepository->nextValId();
$this->userRepository->insertMany($users);
So the question is: is there a one-liner for this use case? Is there a way to tell inside the array of the make that a key has to be different for every model created by the factory? (In this case, I would want $this->userRepository->nextValId() to act for each one of the models created.)
Of course I do not consider using Laravel's factories create method as it would create the register already in the database table and the later insertion would throw a duplicate key exception.
Side note: this is a fake example, not real, so it is possible that there might be a little bug. The code pasted is not proved. Mine is different, but the idea is the same as the one in the example used.
I have found a solution using Laravel's factory sequences:
https://laravel.com/docs/9.x/database-testing#sequences
This would be the cleanest code in my opinion:
$collection = UserModel::factory()->count(4)->state(new Sequence(
fn ($sequence) => ['id' => $this->userRepository->nextValId()]
// Or this using DatabaseTransactions trait: fn ($sequence) => ['id' => $sequence->index + 1]
))->make();
$this->userRepository->insertMany($collection->toArray());

Laravel Eloquent, how to handle UNIQUE error?

I have a MySQL constraint to ensure unique on a composite key. When inserting a new record in my model Foo I get the expected error:
$foo = new Foo(['foo' => 42, 'bar => 1]);
$foo->save();
Error:
SQLSTATE[23000]: Integrity constraint violation: 1062 Duplicate entry '42' for key 'Unique'...
One solution to avoid this error is to query the model before inserting:
if (!Foo::where('foo', 42)->where('bar', 1)->first()) {
$foo = new Foo(['foo' => 42, 'bar => 1]);
$foo->save();
}
Another one would be to catch the exception when preg_match(/UNIQUE/, $e->message) is true.
Is there any better solution?
EDIT
I noticed that in Illuminate\Database\Eloquent\Builder Laravel does the double query anyway which is a bit sad:
public function findOrNew($id, $columns = ['*'])
{
if (! is_null($model = $this->find($id, $columns))) {
return $model;
}
return $this->newModelInstance();
}
In the general case you should be dealing with database errors using the error code and not any regex.
In your particular case pre-querying or using a Laravel method that does that automatically for you, might be preferable if your intention is to overwrite/update existing data.
If you want to generally anticipate an error and handle it you should do something like:
try {
$foo = new Foo(['foo' => 42, 'bar' => 1]);
$foo->save();
} catch (\Exception $e) { // It's actually a QueryException but this works too
if ($e->getCode() == 23000) {
// Deal with duplicate key error
}
}
Refer to https://dev.mysql.com/doc/refman/5.5/en/error-reference.html for an exhaustive list of error codes (but ideally you'd only need to deal with a couple of specific errors and under very specific circumstances.
Lastly the SQL ON DUPLICATE KEY UPDATE might also work for you, however if you are doing this to silently ignore the new values then I suggest you do the error handling instead.
You can use the firstOrCreate method:
$foo = Foo::firstOrCreate(['foo' => 42, 'bar' => 1]);
This will check if the record exists in the database before creating it. If it exists, it will return the record.
For more information: https://laravel.com/docs/5.8/eloquent#inserting-and-updating-models
laravel provides you with the firstOrCreate functions, which first checks if that value exist in the database, then you also have updateOrCreate function to use incase you want to update some value, but most important one, if you want to handle only unique records then check the validation rules, you would do a FormRequest and add unique rule to that field, after that laravel with provide you with error messages, so you handle the error on the client side
The verification need to be done at Controller level (like laravel Validator) and it's better to do a count() instead of the first().
There are different solution depending on what you wanna do when the entity exists in the database. For example you can do updateOrCreate()
$foo = App\Foo::updateOrCreate(['foo' => 42, 'bar' => 1]);
or throw an exception

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

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

Getting all records with a condition so I can display it in cGridView yii

I have a table used for normalization called Sharing
the contents of the table are share_id, pr_id(patient record id) and doctor_id
I want to get the pr_id under a doctor
so I used this code...
$shareModel=Sharing::model()->findByAttributes(array('doctor_id'=>$doctor));
$share= $shareModel->pr_id;
then I changed my model from this:
$criteria->compare('pr_id',$this->pr_id);
into this:
$criteria->compare('pr_id',$share);
and it worked perfectly! However, when I decided to add more patient records under a single doctor.. it would still only display one record so I had to change the code to this
$shareModel=Sharing::model()->findAll(
array(
'condition'=>'doctor_id=:doctor_id',
'params' => array(':doctor_id' => $doctor)
)
);
whenever I try to test it using print_r($shareModel), I get the contents that I want but when I add this line
$share= $shareModel->pr_id;
I get a "Trying to get property of non-object" Error.
can anyone assist?
You will definitely get an error because findAll() method returns array of objects.
It should be:
$share = $shareModel[0]->pr_id;
I would strongly recommend you to use model relations and dataprovider.

Saving with HABTM in CakePHP

I am creating multiple associations in one go and there are a few problems when it comes to saving.
I have the following code:
<?php
foreach($userData as $user) {
$data = array('User' => array('id' => $user['id']), 'Site' => array('id' => $user['site_id']));
$this->User->save($data);
}
?>
I have experimented with formatting the data array in different ways although I always encounter the same problems. Either the previous entries get moved when a new one is inserted or the current one gets updated.
I could just use the following although I need a behavior to trigger.
$this->User->SiteUser->save($data);
Edit: Also $this->User->create(); doesn't seem to do much.
The IRC helped work out what was wrong, once the unique key was set to false everything was able to save correctly.
//In the user model
var $hasAndBelongsToMany = array(
'Site' => array(
'className' => 'Site',
'unique' => false
)
);
Try resetting the id before a new save(), possibly on both models:
$this->User->id = null;
Cake decides whether to update or insert entries based on the set id, and save() sets an id automatically. Not sure why create() doesn't take care of this for you.
Also, if you want to save HABTM data, you should need to use saveAll() instead of save(). Also see this question.

Categories