-
Notifications
You must be signed in to change notification settings - Fork 0
Adding Objects Inserting Data via API
We’ve got an application that we know that we can seed data into. We know that we can query the data that comes out of it. The next and sort of final step is to make sure that we can actually insert new data, and then see the data that comes back out.
So for that we’ll kind of turn back to our controller methods and add some more logic to make sure that those methods actually work correctly. So let’s first go to how we’re going to add a new product to the database. So if we go look at our store
method here in our ProductController
, you can see that the request is being injected into the method itself.
So we’re actually going to use the data that’s inside the request to create our insert record. So without performing any sort of additional validation, all we’ve got to do is to say that a $product
is equal to a `Product::create().
So there’s a create method on our Eloquent models that will actually create a record in the database. And then it accepts an array of attributes. So here we just want to say that the 'name' of the product is going to be equal to what is in the request and has been provided as 'name'. And in fact we can just simply return this. So that’s basically it. So this request, as long as it includes a name value in the request, it will actually create a record and then it should send the result right back out.
/**
* Store a newly created resource in storage.
*
* @param \Illuminate\Http\Request $request
* @return \Illuminate\Http\Response
*/
public function store(Request $request)
{
$product = Product::create([
'name' => $request->input('name')
]);
return $product;
}
We can build a small test to verify that. So let’s just make a quick function, a quick method in our test case to actually test product creation. And so for this one, what we want to do is we don’t want to actually insert the test data first, we just want to make it, and then we’ll insert it and then we’ll use that to make sure that the correct data has actually been put into the database.
public function testProductCreation(){
$product = factory(\App\Product::class)->make();
$this->post(route('products.store'));
}
So we’ll kind of copy our product factory method up here and instead of create let’s just do make. And then what we want to do is we actually want to call the POST method. And so the route for this is going to be API products.store. So what we actually want to do now is we want to create this test creation, this product creation test case, and whenever we actually make this, I want to be explicit about what the name is of the product so that I can say, oh this is exactly what I want to look for cause at this point, that’s really what I care about.
public function testProductCreation(){
$product = factory(\App\Product::class)->make(['name' =>'beets']);
$this->post(route('products.store'));
}
So here I can actually override the defaults so instead of it being something completely random I can just pass an array into this make method and say I want this to be like beets with two e’s. Right? And then, I’ll use this product data to pass into my POST so I want to create a POST request against API product store and the actual data I that I want to use is product JSON serialized.
I’ll spell it correctly this time. And then we need to include some basic jsonheaders
to make sure that that the request is actually recognized as being an Ajax request. So I have those JSON headers defined. So I have these JSON headers defined in our test case. So because we’re doing everything with Ajax, everything is going to be JSON responses and JSON requests. I just want to make sure that I don’t have to type them out all the time. So I’ll just include them in the top of the test case here, and then we’ll just make sure that they are also included in our POST request.
class ExampleTest extends TestCase
{
use DatabaseTransactions;
protected $jsonHeaders = ['Content-Type' => 'application/json', 'Accept' => 'application/json'];
/**
* A basic test example.
*
* @return void
*/
public function testBasicTest()
{
$response = $this->get('/');
$response->assertStatus(200);
}
So after these requests go through, just like our previous ones, we want to make sure that we assert that the response is okay. So that should make sure that we won’t get a bad response and then we also want to make sure that we can see this data in the database. So just like our other helper methods for Database, we have assertDatabaseHas
in database and then we want to make sure that we’re looking in the products database and that we see the actual product name that we thought we were looking for. So create a fake one, try to insert a fake one, make sure we can see in the database, and then make sure the response is okay.
//https://laracasts.com/discuss/channels/testing/badmethodcallexception-method-assertdatabasehas-does-not-exist?page=0#reply-392817
public function testProductCreation(){
$product = factory(\App\Product::class)->make(['name' => 'sbeets']);
$this->post(route('products.store'), $product->toArray(), $this->jsonHeaders);
//$this->assertDatabaseHas('products',['name'=> $product->name]);
}
Adarsh:product-service adarshmaurya$ ./vendor/bin/phpunit
PHPUnit 7.4.4 by Sebastian Bergmann and contributors.
......R 7 / 7 (100%)
Time: 169 ms, Memory: 18.00MB
There was 1 risky test:
1) Tests\Feature\ExampleTest::testProductCreation
This test did not perform any assertions
/Users/adarshmaurya/Playground/play-by-play-laravel-5-getting-started/product-service/tests/Feature/ExampleTest.php:70
OK, but incomplete, skipped, or risky tests!
Tests: 7, Assertions: 13, Risky: 1.
next let's uncomment the last statement:
public function testProductCreation(){
$product = factory(\App\Product::class)->make(['name' => 'beetsss']);
$this->post(route('products.store'), $product->jsonSerialize(), $this->jsonHeaders);
$this->assertDatabaseHas('products',['name'=> $product->name]);
}
Let’s run our test case and see how well we’re doing. So it’s failing. It’s saying okay we can’t see this record in the database. And, you know, that could be any number of problems.
1) Tests\Feature\ExampleTest::testProductCreation
Failed asserting that a row in the table [products] matches the attributes {
"name": "beetsss"
}.
Found: [
{
"id": 1,
"name": "beats",
"created_at": "2018-11-27 21:48:56",
"updated_at": "2018-11-27 21:48:56"
},
{
"id": 2,
"name": "meats",
"created_at": "2018-11-27 21:48:56",
"updated_at": "2018-11-27 21:48:56"
},
{
"id": 3,
"name": "greets",
"created_at": "2018-11-27 21:48:56",
"updated_at": "2018-11-27 21:48:56"
}
] and 6 others.
/Users/adarshmaurya/Playground/play-by-play-laravel-5-getting-started/product-service/vendor/laravel/framework/src/Illuminate/Foundation/Testing/Concerns/InteractsWithDatabase.php:23
/Users/adarshmaurya/Playground/play-by-play-laravel-5-getting-started/product-service/tests/Feature/ExampleTest.php:74
FAILURES!
Tests: 7, Assertions: 14, Failures: 1.
But one of the first things that I want to check, especially when we’re just creating models for the first time, is that sometimes Eloquent wants, it does a lot of things for you, and one of the things that it does is it protects you from having the wrong data injected or just having terrible practices. So it tries to save you.
One of the things that you do need to do though is you need to set, you need to tell the model, whenever you’re doing these create methods, and it’s just sort of mass assigning values to properties of the data, you have to tell it what values are allowed. So if we just go back to our code and then we look in our models, we can go to product, and what we want to do is there’s a property that’s already part of the model itself that says that fillable
is actually nothing. So what we want to do is we want to override that and say fillable
. Like it’s public. And just provide an array that tells us which properties we want to be fillable.
<?php
namespace App;
use Illuminate\Database\Eloquent\Model;
class Product extends Model
{
public $fillable = ['name'];
public function descriptions(){
return $this->hasMany(Description::class);
}
}
We’ll do the same thing to the Description
model and make sure that we let body be fillable
.So we’re just white listing the properties we want to allow to be mass assigned. That’s absolutely it.
<?php
namespace App;
use Illuminate\Database\Eloquent\Model;
class Description extends Model
{
public $fillable = ['body'];
public function product(){
$this->belongsTo(Product::class);
}
public function scopeOfProduct($query, $productId){
return $query->where('product_id',$productId);
}
}
There are ways to white list everything. So if you did want to allow anything to come in, you could do that. You could just put like a wildcard keyword and it would just let everything come through. Like an asterisks. So if we run our test again.
We’re back to everything is working well. Great. So you know, when you start to put these things together there’s some set up, it’s very unlikely that we’d actually have to go into those models and configure the fillable attribute again, but one of the good things about having the test run is they kind of alert you to when things aren’t quite working correctly without you spending too much time debugging them. So we have an insert on our products.
public function testProductDescriptionCreation(){
$product = factory(\App\Product::class)->create(['name' => 'bbbeetsss']);
$descriptions = factory(\App\Description::class)->make();
//$this->post(route('products.store'), $product->toArray(), $this->jsonHeaders)->assertSuccessful();
$this->post(route('products.descriptions.store',['products' =>$product->id]), $descriptions->toArray())->assertSuccessful();
$this->assertDatabaseHas('descriptions',['body'=> $descriptions->body]);
}
Let’s try to insert a product description as well. So we’ll create another test for that. So we can just copy this test case. Duplicate it. And then we’ll just modify it as needed. So in this particular case what we actually want to do is we want to test the product description creation and here we want to actually create the product in the database, so let’s change this from make to create. And then we want to, yet again, we want to create a fake description. This one however, we don’t want to actually insert it into the database, so we’re going to skip that step where we actually take the product, grab the descriptions relationship, and then save many to insert all of them, cause we don’t want them to be in the database. But here we’re just going to create, do a make on the description. And then, when we do make here, let’s actually be specific about what the body is, so here we can just set it to some unique values. Or we can just take the default. It’s probably a little faster. We’ll take the default text that comes from the description.
class ProductDescriptionController extends Controller
{
...
public function store($productId, Request $request)
{
$description = Description::create([
'product_id' => $productId,
'body' => $request->input('body')
]);
return $description;
}
And so now what we want to do is we want to post this data into the API products .descriptions store. And then, just like we did in the previous descriptions route, we want to indicate which product, the resource that we’re going to use, and that product is going to be the product ID that we created. And we’re going to provide the description JSON serialized values.
class Description extends Model
{
public $fillable = ['product_id','body'];
public function product(){
$this->belongsTo(Product::class);
}
And then we’re going to include our JSON headers again. And so now what we should see is inside of the descriptions table, we should see a body that is equal to the description body. Let’s run our tests.
So now, we’ve run our tests. It’s going to fail. But it’s failing because we actually haven’t implemented that method correctly. So let’s go back to our code and let’s make sure that we can build it in a way that will actually function correctly. So here in our store
method, inside of our ProductDescriptionsController
, what we want to do is we want to make sure that we grab hold of our product ID from the request, and make sure that we have the request itself. And then we’re going to include our, just like we did in the product the ProductController
for inserting our products, we’re going to create a description and then we’re going to actually create it where name name is equal to the input or body is equal to the body. This isn’t a very elegant way to do it, by the way, but. So just like we did in the product controller when we’re inserting the product itself, we want to do the same sort of thing with the description. A really fast way to do it is just to set the product ID equal to the product that’s brought in and then set the body to the input from the body itself. Now the product ID is going to throw a problem if we’re just simply inserting the product ID as being some sort of value instead of setting the relationship. So in order to make this version work, we will have to go in and include product ID inside of our fillable.
And now our test case should run successfully. Cool. So that’s one way to do it. We know that our tests pass. We know that we can create our description. We can add our product ID to be fillable. And so at that point you’re just kind of opening up the API to make sure that the product ID can be anything. Right? But when you’re actually building the system, you want to make sure that the product exists first, so at this point if we dialed in API slash products slash a non-existent ID and then just posted this, it would work correctly because there’s no product there but it’s just inserting a value for what the product ID is. Our foreign key index would probably prevent that. It might throw an error when that happens.
But what we want to do is we want the application to be a little bit smarter about how it’s going to actually handle this, so instead of actually just creating the raw description with the product ID set explicitly, what we want to do is we’ll include our product model in this class, in this controller, and then we’ll use another Eloquent method called findOrFail that will actually look for the product. If it doesn’t find it, it will simply return an error and say that this is no longer a satisfiable request. We can’t actually do anything with this. But if it does find it, we can attach it in the proper way so that we can ensure that there’s no way for the data to get inserted if it’s not actually valid. So here we can say product is going to be equal to the product model method findOrFail and then we’ll just include the product ID and then here instead of saying create brand new description what we want to do is we want to do attempt to say product and then we’ll grab that descriptions relationship object again, so product descriptions and then save and then here we’ll actually create new description with just the body assigned. And then, for the sake of this, we’ll just return the product descriptions.
public function store($productId, Request $request)
{
$product = Product::findorFail($productId);
$product->descriptions()->save(new Description([
'body' => $request->input('body')
]));
// $description = Description::create([
// 'product_id' => $productId,
// 'body' => $request->input('body')
// ]);
// return $description;
return $product->descriptions;
}
So let’s go back to our test cases and let’s make sure that they still pass.
Adarsh:product-service adarshmaurya$ ./vendor/bin/phpunit
PHPUnit 7.4.4 by Sebastian Bergmann and contributors.
........ 8 / 8 (100%)
Time: 183 ms, Memory: 16.00MB
OK (8 tests, 17 assertions)
Adarsh:product-service adarshmaurya$
Everything’s still good.