The source code is on github bitfumes/api
- The purpose of these notes are to follow the above tutorial, making detailed notes of each step.
- They are not verbatim of the original video.
- Although the notes are detailed, it is possible they may not make sense out of context.
- The notes are not intended as a replacement the video
- Notes are more of a companion
- They allow an easy reference search.
- Allowing a particular video to be found and re-watched.
- Code snippets are often used to highlight the code changed, any code prior or post the code snipped is generally unchanged from previous notes, or to highlight only the output of interest. To signify a snippet of a larger code block, dots are normally used e.g.
\\ ...
echo "Hello";
\\ ...
Navigate to the route folder, using a terminal, then run the following commands:
laravel new eapi
This will create a new laravel project called eapi, once installed change directory and commit to git.
cd eapi
git init
git add .
To create a Generate a migration, factory, and resource controller for the model use the -a flag (see php artisan help make:model for full help)
php artisan make:model Model/Product -a
> Model created successfully.
> Factory created successfully.
> Created Migration: 2019_02_06_154700_create_products_table
> Controller created successfully.
php artisan make:model Model/Review -a
> Model created successfully.
> Factory created successfully.
> Created Migration: 2019_02_06_154851_create_reviews_table
> Controller created successfully.
The Model, Factory, Migration and Controllers are all created. Next add a route to api.php
Route::Resource('/products', 'ProductController');
Check the routes from the terminal:
php artisan route:list
Domain | Method | URI | Name | Action | Middleware |
---|---|---|---|---|---|
GET | HEAD | / | Closure | web | ||
GET | HEAD | api/products | products.index | App\Http\Controllers\ProductController@index | api | |
POST | api/products | products.store | App\Http\Controllers\ProductController@store | api | |
GET | HEAD | api/products/create | products.create | App\Http\Controllers\ProductController@create | api | |
GET | HEAD | api/products/{product} | products.show | App\Http\Controllers\ProductController@show | api | |
PUT | PATCH | api/products/{product} | products.update | App\Http\Controllers\ProductController@update | api | |
DELETE | api/products/{product} | products.destroy | App\Http\Controllers\ProductController@destroy | api | |
GET | HEAD | api/products/{product}/edit | products.edit | App\Http\Controllers\ProductController@edit | |
GET | HEAD | api/user | Closure |
To check the Routes required for an apiResource change the php in api.php to:
Route::apiResource('/products', 'ProductController');
Then run artisan route:list again
php artisan route:list
Domain | Method | URI | Name | Action | Middleware |
---|---|---|---|---|---|
GET | HEAD | / | Closure | ||
GET | HEAD | api/products | products.index | App\Http\Controllers\ProductController@index | |
POST | api/products | products.store | App\Http\Controllers\ProductController@store | api | |
GET | HEAD | api/products/{product} | products.show | App\Http\Controllers\ProductController@show | |
PUT | PATCH | api/products/{product} | products.update | App\Http\Controllers\ProductController@update | |
DELETE | api/products/{product} | products.destroy | App\Http\Controllers\ProductController@destroy | api | |
GET | HEAD | api/user | Closure |
Note: There is no edit or create in the list.
Obviously you will have the create function and that's the edit function because the response controller will create them automatically, they can be deleted for an API. If required edit the ProductController.php and remove the edit function.
Next setup the Route for reviews, this will be a product group in the layout of /products/{id}/reviews. Edit the api.php file and add the following route:
Route::group(['prefix'=>'products'], function() {
Route::apiResource('/{product}/reviews', 'ReviewController');
})
php artisan route:list
Domain | Method | URI | Name | Action | Middleware |
---|---|---|---|---|---|
GET|HEAD | / | Closure | web | ||
GET|HEAD | api/products | products.index | App\Http\Controllers\ProductController@index | api | |
POST | api/products | products.store | App\Http\Controllers\ProductController@store | api | |
GET|HEAD | api/products/{product} | products.show | App\Http\Controllers\ProductController@show | api | |
PUT|PATCH | api/products/{product} | products.update | App\Http\Controllers\ProductController@update | api | |
DELETE | api/products/{product} | products.destroy | App\Http\Controllers\ProductController@destroy | api | |
GET|HEAD | api/products/{product}/reviews | reviews.index | App\Http\Controllers\ReviewController@index | api | |
POST | api/products/{product}/reviews | reviews.store | App\Http\Controllers\ReviewController@store | api | |
GET|HEAD | api/products/{product}/reviews/{review} | reviews.show | App\Http\Controllers\ReviewController@show | api | |
PUT|PATCH | api/products/{product}/reviews/{review} | reviews.update | App\Http\Controllers\ReviewController@update | api | |
DELETE | api/products/{product}/reviews/{review} | reviews.destroy | App\Http\Controllers\ReviewController@destroy | api | |
GET|HEAD | api/user | Closure | api,auth:api |
git status
View the status of Git. To add all and commit the changes:
git add .
git commit -m "Created Model -a
git remote add api <https://github.com/Pen-y-Fan/api>
git push api
Update the database settings, edit database/ migrations / 2019_02_06_154700_create_products_table, add the products database fields between 'id' and timestamps :
$table->increments('id');
$table->string('name');
$table->text('detail');
$table->integer('price');
$table->integer('stock');
$table->integer('discount');
$table->timestamps();
Same for 2019_02_06_154851_create_reviews_table, add the review fields between id and timestamps.
$table->increments('id');
$table->integer('product_id')->unsigned()->index();
$table->foreign('product_id')->references('id')->on('products')->onDelete('cascade');
$table->string('customer');
$table->text('review');
$table->integer('star');
$table->timestamps();
In PHPMyAdmin create a database called eapi, with utf8mb4_unicode_ci
Edit the .env file with the database, adjust the MySQL user and password as required.
DB_CONNECTION=mysql
DB_HOST=127.0.0.1
DB_PORT=3306
DB_DATABASE=eapi
DB_USERNAME=root
DB_PASSWORD=
From a shell run migrate:
php artisan migrate
> [32mMigration table created successfully.[39m
> [33mMigrating:[39m 2014_10_12_000000_create_users_table
> [32mMigrated:[39m 2014_10_12_000000_create_users_table
> [33mMigrating:[39m 2014_10_12_100000_create_password_resets_table
> [32mMigrated:[39m 2014_10_12_100000_create_password_resets_table
> [33mMigrating:[39m 2019_02_06_154700_create_products_table
> [32mMigrated:[39m 2019_02_06_154700_create_products_table
> [33mMigrating:[39m 2019_02_06_154851_create_reviews_table
> [32mMigrated:[39m 2019_02_06_154851_create_reviews_table
Check PHPMyAdmin and the tables will be created. Next add to github
git add .
git commit -m "Migration created for Products and Reviews"
> [master efdd09b] Migration created for Products and Reviews
> 3 files changed, 17 insertions(+), 7 deletions(-)
> delete mode 100644 Untitled-1.md
git push
> Enumerating objects: 11, done.
> Counting objects: 100% (11/11), done.
> Delta compression using up to 4 threads
> Compressing objects: 100% (6/6), done.
> Writing objects: 100% (6/6), 743 bytes | 743.00 KiB/s, done.
> Total 6 (delta 5), reused 0 (delta 0)
> remote: Resolving deltas: 100% (5/5), completed with 5 local objects.
> To <https://github.com/Pen-y-Fan/api>
> bed45ab..efdd09b master -> master
Use the faker library built into Laravel to create some content.
Edit \eapi\database\factories\ ProductFactory.php
use Faker\Generator as Faker;
...
$factory->define(App\Model\Product::class, function (Faker $faker) {
return [
'name' => $faker->word,
'detail' => $faker->paragraph,
'price' => $faker->numberBetween(100, 1000),
'stock' => $faker->randomDigit,
'discount' => $faker->numberBetween(2, 30)
];
});
Next ReviewFactory.php, see Laravel documentation on factory e.g.
<?php
use App\Model\Product;
use Faker\Generator as Faker;
$factory->define(App\Model\Review::class, function (Faker $faker) {
return [
'product_id' => function () {
return Product::all()->random();
},
'customer' => $faker->name,
'review' => $faker->paragraph,
'star' => $faker->numberBetween(0, 5)
];
});
Next update the DatabaseSeeder.php file. Comment out the users seeder, and add the two new Products and Reviews factories.
class DatabaseSeeder extends Seeder
{
/**
* Seed the application's database.
*
* @return void
*/
public function run()
{
// $this->call(UsersTableSeeder::class);
factory(App\Model\Product::class, 50)->create();
factory(App\Model\Review::class, 300)->create();
}
}
Run the artisan command to seed the database:
php artisan db:seed
> Database seeding completed successfully
The above will take a minute to run, with no activity, be patient. Refresh the eapi database in PHPMyAdmin to see the tables have been populated.
As before commit the changes to git, this time use VS Code to commit all three items, and use the three dots menu to push to Github. Check Output and select Git to view any errors. Check github for the commits.
Get them back to victims and in this we are going to create the relationship between projects and reviews and with that and your post and this is the general idea the vault level and the cool thing about laravel.
Open App/Model/ Product.php create the reviews function, so Products hasMany Reviews.
use App\Model\Review;
// ...
class Product extends Model
{
public function reviews()
{
return $this->hasMany(Review::class);
}
}
Open App/Model/ Review.php, create the product function, review belongs to Product::class and add the namespace.
// ...
use App\Model\Product;
use Illuminate\Database\Eloquent\Model;
class Review extends Model
{
public function product()
{
return $this->belongsTo(Product::class);
}
}
Next test the models are working using tinker, from the terminal.
php artisan tinker
App/Model/Product
> PHP Warning: Use of undefined constant App - assumed 'App' (this will throw an
> Error in a future version of PHP) in Psy Shell code on line 1
App\Model\Product::find(4)
> >>> App\Model\Product::find(4)
> => App\Model\Product {#2927
> id: 4,
> name: "tenetur",
> detail: "Itaque quibusdam aperiam consequatur beatae maxime. Sint sed praes
> entium possimus. Est dolorem laudantium rerum quia.",
> price: 491,
> stock: 8,
> discount: 20,
> created_at: "2019-02-07 17:41:31",
> updated_at: "2019-02-07 17:41:31",
> }
Product 4 returns the product record for id: 4.
Next fins the reviews for product 4:
App\Model\Product::find(4)->reviews
> => Illuminate\Database\Eloquent\Collection {#2930
> all: [
> App\Model\Review {#2933
> id: 435,
> product_id: 4,
> customer: "Dr. Emmanuelle Turner",
> review: "Inventore et pariatur in corporis blanditiis iste. Et deleniti
> consectetur quia omnis neque similique repudiandae dolores. Consequatur et unde
> quis totam pariatur quis nemo. Sunt soluta exercitationem voluptas velit natus
> dolorum provident. A et ducimus saepe at ut sunt incidunt perspiciatis.",
> star: 4,
> created_at: "2019-02-07 18:30:48",
> updated_at: "2019-02-07 18:30:48",
> },
> App\Model\Review {#2934
> id: 469,
> product_id: 4,
> ....
> },
> ],
> }
Next check review 4 and find the product.
App\Model\Review::find(4)->product
> => App\Model\Product {#2928
> id: 77,
> name: "nesciunt",
> detail: "Exercitationem consequuntur excepturi rerum magni fuga. Ut id null
> a rerum. Nulla ut voluptatibus dolores eum consequatur quis accusantium.",
> price: 596,
> stock: 9,
> discount: 13,
> created_at: "2019-02-07 17:43:41",
> updated_at: "2019-02-07 17:43:41",
> }
A product of ID: 77 was found.
Next for product 77 the reviews
App\Model\Product::find(77)->reviews
> >>> App\Model\Product::find(77)->reviews
> => Illuminate\Database\Eloquent\Collection {#2935
> all: [
> App\Model\Review {#2941
> id: 4,
> product_id: 77,
> customer: "Dr. Benny Bernier",
> review: "Odio dolore labore fugit aliquam. Recusandae nulla repellat qu
> ia eos molestiae a quia consequuntur. Asperiores consequatur fugit qui ut quia p
> laceat adipisci. Autem placeat dolores et.",
> star: 5,
> created_at: "2019-02-07 18:27:12",
> updated_at: "2019-02-07 18:27:12",
> },
> App\Model\Review {#2929
> id: 19,
> product_id: 77,
> .....
Above proves the relationships are working.
Check Laravel documentation, see the Resources documentation, to make a resource for help run php artisan help make:resource, to create the ProductCollection resource run:
php artisan make:resource Product/ProductCollection
Resource collection created successfully.
A productionCollection will be created. Open the app\Http\Resources\Product\ ProductCollection.php file created.
Next open the ProductController.php, amend the index method to return all products:
public function index()
{
return Product::all();
}
To check the resource is working, run artisan serve and then open the web page to api/products
php artisan serve
http://127.0.0.1:8000/api/products
Open the website to api/product to see all the products, however this is a bad API, the current output give the product ID and created dates, this need to be restricted.
Run php artisan again, this time make:resource Product/ProductResource
php artisan make:resource Product/ProductResource
Resource created successfully.
This will extend Resource, the previous one extended resourceCollection.
Open app\Http\Resources\Product\ ProductResource.php
Change the output to an array with the required fields, e.g. name, description (for the detail field), price, stock and discount, also add the namespace for the ProductResource.
use App\Http\Resources\Product\ProductResource;
// ....
public function toArray($request)
{
return [
'name' => $this->name,
'description' => $this->detail,
'price' => $this->price,
'stock' => $this->stock,
'discount' => $this->discount
];
}
}
Next edit the app\Http\Controllers\ ProductController.php. To transform a single product edit the show method. First set the return for $product
public function show(Product $product)
{
return $product;
}
Open the website and navigate the url to http://127.0.0.1:8000/api/products/4
id 4 name "tenetur" detail "Itaque quibusdam aperiam consequatur beatae maxime. Sint sed praesentium possimus. Est dolorem laudantium > rerum quia." price 491 stock 8 discount 20 created_at "2019-02-07 17:41:31" updated_at "2019-02-07 17:41:31"
Now edit the ProductController.php show method again, and change to the the resource (transform) method:
public function show(Product $product)
{
return new ProductResource($product);
}
Now the output is transformed:
data name "tenetur" description "Itaque quibusdam aperiam consequatur beatae maxime. Sint sed praesentium possimus. Est dolorem > laudantium rerum quia." price 491 stock 8 discount 20
Note description is the field for detail, also the record is wrapped inside a data record.
Commit to git as Product Resource created and push.
To check the route created for reviews open a terminal and check the routes:
php artisan route:list
Method | URI | Name | Action | Middleware |
---|---|---|---|---|
... | ||||
GET | HEAD | api/products/{product}/reviews | reviews.index | App\Http\Controllers\ReviewController@index |
... |
Edit ProductResource.php and add a href (link) to the reviews route, add after the discount field, also calculate the current selling price (listPrice - discount %) and finally if there is no stock output 'Out of Stock'
public function toArray($request)
{
return [
'name' => $this->name,
'description' => $this->detail,
'listPrice' => $this->price,
'discount(%)' => $this->discount,
'purchasePrice' => round(
(1 - $this->discount / 100) * $this->price,
2
),
'stock' => $this->stock == 0 ? 'Out of Stock' : $this->stock,
'rating' =>
$this->reviews->count() > 0
? round($this->reviews->avg('star'), 2)
: 'No rating yet',
'href' => [
'reviews' => route('reviews.index', $this->id)
]
];
}
Navigating to http://127.0.0.1:8000/api/products/4 now displays:
data name "tenetur" description "Itaque quibusdam aperiam consequatur beatae maxime. Sint sed praesentium possimus. Est dolorem laudantium rerum quia." listPrice 491 discount(%) 20 purchasePrice 392.8 stock 8 rating 1.75 href
Navigating to http://127.0.0.1:8000/api/products/6 will display (Note Out of Stock):
data name "consequatur" description "Omnis expedita aliquid soluta aperiam temporibus eum. Ut quod minima velit totam quos. Ex at nam ullam mollitia." listPrice 667 discount(%) 16 purchasePrice 560.28 stock "Out of Stock" rating 1.8 href reviews "http://127.0.0.1:8000/api/products/6/reviews"
The link will not work, yet. That will be part of a future lecture.
As usual commit "Modified Product details" to Git and Push to Github.
This lecture will create a list of all products, with only their name, description, purchasePrice, discount and link.
Currently products look like this:
0 id 1 name "sed" detail "Enim qui et doloremque quas nobis eligendi voluptas velit. Quia totam modi consectetur ad sunt perspiciatis. Quia necessitatibus sunt corporis velit." price 589 stock 5 discount 29 created_at "2019-02-07 17:41:31" updated_at "2019-02-07 17:41:31" 1 id 2 name "illo" detail "Sunt qui laboriosam nemo labore iusto voluptatum saepe molestias. Commodi modi possimus et enim ab officia aliquid odio. Rerum quibusdam nulla in aut culpa aut ratione." price 369 stock 3 discount 6 created_at "2019-02-07 17:41:31" updated_at "2019-02-07 17:41:31" ....
ProductController.php return the ProductCollection add the namespace and amend the return object:
use App\Http\Resources\Product\ProductCollection;
\\...
public function index()
{
return ProductCollection::collection(Product::all());
}
Next update ProductCollection, change ResourceCollection to Resource and update the return object. For the link see route:list for the correct route to 'products.show'.
use Illuminate\Http\Resources\Json\Resource;
class ProductCollection extends Resource
{
/**
* Transform the resource collection into an array.
*
* @param \Illuminate\Http\Request $request
* @return array
*/
public function toArray($request)
{
return [
'name' => $this->name,
'purchasePrice' => round((1 - $this->discount / 100) * $this->price, 2),
'rating' =>
$this->reviews->count() > 0 ? round($this->reviews->avg('star'), 2) : 'No rating yet',
'discount(%)' => $this->discount,
'href' => [
'reviews' => route('products.show', $this->id)
]
];
}
}
data 0 name "sed" purchasePrice 418.19 rating 3.2 discount(%) 29 href
reviews "http://127.0.0.1:8000/api/products/1" 1 name "illo" purchasePrice 346.86 rating 3.5 discount(%) 6 href reviews "http://127.0.0.1:8000/api/products/2" ...
Commit to Git "Product Collection Resource" and push.
In this lesson the Reviews API will be created and transformed. First create the Review Resource:
php artisan make:resource ReviewResource
Resource created successfully.
Edit the ReviewController.php, to test it return Review::all()
public function index()
{
return Review::all();
}
This will return every review:
0 id 1 product_id 82 customer "Prof. Hayley Brakus PhD" review "Voluptate porro tempore …ipsa dolorem veritatis." star 3 created_at "2019-02-07 18:27:12" updated_at "2019-02-07 18:27:12" 1 id 2 product_id 93 customer "Orie Sporer" review "Saepe et iure explicabo …t omnis aspernatur aut." star 1 created_at "2019-02-07 18:27:12" updated_at "2019-02-07 18:27:12"
We want the review for the product, update the method to use the product id:
use App\Model\Product;
//...
public function index(Product $product)
{
return $product->reviews;
}
This give us the reviews for the product. http://127.0.0.1:8000/api/products/6/reviews
0 id 222 product_id 6 customer "Dr. Nash Shanahan III" review "Deserunt ut excepturi quos consequatur qui culpa. Sapiente fugiat quis mollitia cupiditate sint veritatis in vitae. Non quas officiis vero eos." star 1 created_at "2019-02-07 18:27:15" updated_at "2019-02-07 18:27:15" 1 id 474 product_id 6 customer "Polly Carter V" review "Unde similique dolores fugiat impedit. Distinctio cumque doloremque in numquam quia. Quae et est et illo. Officiis voluptas ut voluptatum nostrum non." star 4 created_at "2019-02-07 18:30:49" updated_at "2019-02-07 18:30:49" 2
Now to transform the reviews. Open the ReviewResource.php
public function toArray($request)
{
return [
'customer' => $this->customer,
'body' => $this->review,
'star' => $this->star
];
}
Return to ReviewController.php and wrap the review around the ReviewResource
use App\Http\Resources\ReviewResource;
// ...
public function index(Product $product)
{
return ReviewResource::collection($product->reviews);
}
The reviews have now been transformed, http://127.0.0.1:8000/api/products/6/reviews:
data 0 customer "Dr. Nash Shanahan III" body "Deserunt ut excepturi quos consequatur qui culpa. Sapiente fugiat quis mollitia cupiditate sint veritatis in vitae. Non quas officiis vero eos." star 1 1 customer "Polly Carter V" body "Unde similique dolores fugiat impedit. Distinctio cumque doloremque in numquam quia. Quae et est et illo. Officiis voluptas ut voluptatum nostrum non." star 4
If all products are viewed, they will all be displayed, to limit this use paginate, edit ProductController.php
public function index()
{
return ProductCollection::collection(Product::paginate(20));
}
Viewing products now limits to 20 per page, with links:
data 0 name "sed" purchasePrice 418.19 rating 3.2 discount(%) 29 href reviews "http://127.0.0.1:8000/api/products/1" 1 name "illo" purchasePrice 346.86 rating 3.5 discount(%) 6 href reviews "http://127.0.0.1:8000/api/products/2" ... 19 name "vitae" purchasePrice 183.08 rating 3.2 discount(%) 8 href reviews "http://127.0.0.1:8000/api/products/20" links first "http://127.0.0.1:8000/api/products?page=1" last "http://127.0.0.1:8000/api/products?page=5" prev null next "http://127.0.0.1:8000/api/products?page=2" meta current_page 1 from 1 last_page 5 path "http://127.0.0.1:8000/api/products" per_page 20 to 20 total 100
Commit to Git and push.
Goto Laravel documentation and view the documention on how to install passport: API Authentication (Passport)
composer require laravel/passport
Using version ^7.1 for laravel/passport ./composer.json has been updated Loading composer repositories with package information Updating dependencies (including require-dev) Package operations: 7 installs, 0 updates, 0 removals
- Installing symfony/psr-http-message-bridge (v1.1.0): Downloading (connecting Downloading (100%)
- Installing phpseclib/phpseclib (2.0.14): Downloading (100%)
- Installing defuse/php-encryption (v2.2.1): Downloading (100%)
- Installing league/event (2.2.0): Downloading (100%)
- Installing league/oauth2-server (7.3.2): Downloading (100%)
- Installing firebase/php-jwt (v5.0.0): Downloading (100%)
- Installing laravel/passport (v7.1.0): Downloading (100%) symfony/psr-http-message-bridge suggests installing psr/http-factory-implementat ion (To use the PSR-17 factory) phpseclib/phpseclib suggests installing ext-libsodium (SSH2/SFTP can make use of some algorithms provided by the libsodium-php extension.) phpseclib/phpseclib suggests installing ext-mcrypt (Install the Mcrypt extension in order to speed up a few other cryptographic operations.) phpseclib/phpseclib suggests installing ext-gmp (Install the GMP (GNU Multiple P recision) extension in order to speed up arbitrary precision integer arithmetic operations.) Writing lock file Generating optimized autoload files Illuminate\Foundation\ComposerScripts::postAutoloadDump @php artisan package:discover --ansi Discovered Package: ←[32mbeyondcode/laravel-dump-server←[39m Discovered Package: ←[32mfideloper/proxy←[39m Discovered Package: ←[32mlaravel/nexmo-notification-channel←[39m Discovered Package: ←[32mlaravel/passport←[39m Discovered Package: ←[32mlaravel/slack-notification-channel←[39m Discovered Package: ←[32mlaravel/tinker←[39m Discovered Package: ←[32mnesbot/carbon←[39m Discovered Package: ←[32mnunomaduro/collision←[39m ←[32mPackage manifest generated successfully.←[39m
php artisan migrate
Migrating: 2016_06_01_000001_create_oauth_auth_codes_table Migrated: 2016_06_01_000001_create_oauth_auth_codes_table Migrating: 2016_06_01_000002_create_oauth_access_tokens_table Migrated: 2016_06_01_000002_create_oauth_access_tokens_table Migrating: 2016_06_01_000003_create_oauth_refresh_tokens_table Migrated: 2016_06_01_000003_create_oauth_refresh_tokens_table Migrating: 2016_06_01_000004_create_oauth_clients_table Migrated: 2016_06_01_000004_create_oauth_clients_table Migrating: 2016_06_01_000005_create_oauth_personal_access_clients_table Migrated: 2016_06_01_000005_create_oauth_personal_access_clients_table
Check PHPMyAdmin, the tables are all empty. Next install passport.
php artisan passport:install
Encryption keys generated successfully. Personal access client created successfully. Client ID: 1 Client secret: 9oUf9HIbgtt3yhMUgokI2X7kbQO2S1y5zUUQivoP Password grant client created successfully. Client ID: 2 Client secret: lIWOi1OkvgRrhRN825CtshKECTtvaawdU8bNiesw
Next, following the documentation, edit User.php add HasApiTokens to the User class.
// ...
use Laravel\Passport\HasApiTokens; // Add
use Illuminate\Foundation\Auth\User as Authenticatable;
class User extends Authenticatable
{
use HasApiTokens, Notifiable; // Add HasApiTokens
// ...
Following the next step in the documentation we need to add Passport::routes method within the boot method of your AuthServiceProvider.php
use Laravel\Passport\Passport; // Add
use Illuminate\Support\Facades\Gate;
// ...
public function boot()
{
$this->registerPolicies();
Passport::routes(); // Add
}
// ...
Finally, in your config/ auth.php configuration file, you should set the driver to passport
// ...
'guards' => [
'web' => [
'driver' => 'session',
'provider' => 'users',
],
'api' => [
'driver' => 'passport', // Changed from 'token' to 'passport'
'provider' => 'users',
],
],
// ...
Before users can be granted access we need to create the user authorisation:
php artisan make:auth
Authentication scaffolding generated successfully.
Start the web service (if it isn't already running)
php artisan serve
Open the browser to localhost:8000 click Register in the top right corner and register a new user. e.g.
register | |
---|---|
Name | Mickey Mouse |
E-Mail Address | [email protected] |
Password : | 123456 |
Confirm Password: | 123456 |
Now use Postman, create another folder within EAPI, call it OAuth.
Create a POST request to localhost:8000/oauth/token. The header needs two keys:
key | value | description |
---|---|---|
Accept | application/json | |
Content-Type | application/json |
For Body the following needs to be passed as Raw:
{
"grant_type": "password",
"client_id": 2,
"client_secret": "lIWOi1OkvgRrhRN825CtshKECTtvaawdU8bNiesw",
"username": "[email protected]",
"password": "123456"
}
The client ID and secret can be taken from the output above, for passport:install or by viewing the oauth_clients table:
SELECT *
FROM `oauth_clients`
WHERE name = "Laravel Password Grant Client"
Click Send.
The response is:
{
"token_type": "Bearer",
"expires_in": 31536000,
"access_token": "eyJ0eXAiOiJKV1QiLCJhbGciOiJSUzI1NiIsImp0aSI6ImIxMDc0ZTBlNzgwMDFkNTVhMzhkMGU5ODZjNTdmZTkwM2NkOTlhNTQ1NzAxNzZkN2Y1NDFlOTI5Y2FkYmQ4ZWVhZWU4MTg5ZmUzODc3OGRjIn0.eyJhdWQiOiIyIiwianRpIjoiYjEwNzRlMGU3ODAwMWQ1NWEzOGQwZTk4NmM1N2ZlOTAzY2Q5OWE1NDU3MDE3NmQ3ZjU0MWU5MjljYWRiZDhlZWFlZTgxODlmZTM4Nzc4ZGMiLCJpYXQiOjE1NDk2Mjc3MTgsIm5iZiI6MTU0OTYyNzcxOCwiZXhwIjoxNTgxMTYzNzE4LCJzdWIiOiIxIiwic2NvcGVzIjpbXX0.rx7GLBhZmt8lLI0aEpGSS1gpXVckPDSk9XGG_BuywI7EQfUcY0azrMKQJOCyXOnDc_yPpiX0ivlzPgqNzM7bEQm2LDKOlHnOtCvah2klUnAZpHsaUrKoEpsDgdnVWifJ4KpyjppIaXU7BXIFFObmO35MZDOVX4zDmb2n8FOfiEWM5QSVn85k_Br1xdpegzgAFOJ6HNoi6egB5XJ55cGBC9LZvNnO_woTqnXz7Dntbm2wDQX-cGapv-OCqWHXq8BLFGvDxiQvHjDkjLAjIsl5V57INUbb_tO6z2TttzMgK8nXjrfZjS3JzrMiZOhQrLXeZ4AJ2TO6DAZ-N8m4-oaEc-st-kZrdmi6JmZT0VjkShoXNitV3yQA8r7lZDZ-urwWpGl82-6p0TXqJR707SepEH_2uL2GPpEcwB86qASM0Iko0jIyE-EoVG8rR2nSH7c-sYyc4wsQSDT26phb-I_I_6OXZwxLV-pZa8F_by77DnOgyJ3htgjf0uKERxt8KOIvYyymRjkAHTTqbUIMY4PxuKllgj_XL2hRvbF23bcV-c4y94Jhq3Z-Ojdk6wf-zfN1ErdKUE7KjpWPI3DkrZhCZuNrOcWK2J_MOj_n9Gzt8URJ9i-8mxMvS3C9h60SBR1LUcujXyi6_e_4L4ZSKCPVOl9RKZJOdQgg-JBfNexr_y0",
"refresh_token": "def50200abded07837f03989a4af7365fff2469990a8c744641424688f6758c3e7dc1395f7173dbf343a4ce22138ce08bf8513200cf63647e347b079c6c860fb0224e84002cd0399d78ab4da353584240504fc38db6cb8f00821573106e3cabd08f302d369eba337ed01a0c8a8edcd4d34939ca5efa7d3effb23902d57f73f85b43cc780cb444132076be894ac06a79a33eb98cd4bc29feac0a5e4764e956d58ffd9758b4f884bb3882b833b79a3a37dc091cc36c2a32f11ed63615e59e5f651ec02a1ef0f62d9710ebfacde7c30bc4d0552c6238164865f94c5e973519b39549395a8b96156939f3c56d916efbdac634b473d0681f9fd40bcbfb01914d84489f09bda7ac61e876b4e3a93b245b098e27a773f7304a17adb3c12d1f5721c6d85fef44308d4c7662fd4cbe1b2c9ef25382354a75c71618b23de214657835aab669c9b822576d4094e3fdffd879e6c39188d270de3a60f65b5088c6b15e4ecca8812"
}
Next take the access_token and create a Get request to localhost:8000/api/user
key | value | description |
---|---|---|
Accept | application/json | |
Content-Type | application/json | |
Authorization | Bearer eyJ0eXAiOiJKV1QiLCJhbGciOiJSUzI1NiIsImp0aSI6ImIxMDc0ZTBlNzgwMDFkNTVhMzhkMGU5ODZjNTdmZTkwM2NkOTlhNTQ1NzAxNzZkN2Y1NDFlOTI5Y2FkYmQ4ZWVhZWU4MTg5ZmUzODc3OGRjIn0.eyJhdWQiOiIyIiwianRpIjoiYjEwNzRlMGU3ODAwMWQ1NWEzOGQwZTk4NmM1N2ZlOTAzY2Q5OWE1NDU3MDE3NmQ3ZjU0MWU5MjljYWRiZDhlZWFlZTgxODlmZTM4Nzc4ZGMiLCJpYXQiOjE1NDk2Mjc3MTgsIm5iZiI6MTU0OTYyNzcxOCwiZXhwIjoxNTgxMTYzNzE4LCJzdWIiOiIxIiwic2NvcGVzIjpbXX0.rx7GLBhZmt8lLI0aEpGSS1gpXVckPDSk9XGG_BuywI7EQfUcY0azrMKQJOCyXOnDc_yPpiX0ivlzPgqNzM7bEQm2LDKOlHnOtCvah2klUnAZpHsaUrKoEpsDgdnVWifJ4KpyjppIaXU7BXIFFObmO35MZDOVX4zDmb2n8FOfiEWM5QSVn85k_Br1xdpegzgAFOJ6HNoi6egB5XJ55cGBC9LZvNnO_woTqnXz7Dntbm2wDQX-cGapv-OCqWHXq8BLFGvDxiQvHjDkjLAjIsl5V57INUbb_tO6z2TttzMgK8nXjrfZjS3JzrMiZOhQrLXeZ4AJ2TO6DAZ-N8m4-oaEc-st-kZrdmi6JmZT0VjkShoXNitV3yQA8r7lZDZ-urwWpGl82-6p0TXqJR707SepEH_2uL2GPpEcwB86qASM0Iko0jIyE-EoVG8rR2nSH7c-sYyc4wsQSDT26phb-I_I_6OXZwxLV-pZa8F_by77DnOgyJ3htgjf0uKERxt8KOIvYyymRjkAHTTqbUIMY4PxuKllgj_XL2hRvbF23bcV-c4y94Jhq3Z-Ojdk6wf-zfN1ErdKUE7KjpWPI3DkrZhCZuNrOcWK2J_MOj_n9Gzt8URJ9i-8mxMvS3C9h60SBR1LUcujXyi6_e_4L4ZSKCPVOl9RKZJOdQgg-JBfNexr_y0 |
{
"id": 1,
"name": "Mickey Mouse",
"email": "[email protected]",
"email_verified_at": null,
"created_at": "2019-02-08 11:19:48",
"updated_at": "2019-02-08 11:19:48"
}
Still in postman create new environment (cog in the top right > Manage Environments), add a new environment called EAPI:
key | value |
---|---|
auth | Bearer eyJ0eXAiOiJKV1QiLCJhbGciOiJSUzI1NiIsImp0aSI6ImIxMDc0ZTBlNzgwMDFkNTVhMzhkMGU5ODZjNTdmZTkwM2NkOTlhNTQ1NzAxNzZkN2Y1NDFlOTI5Y2FkYmQ4ZWVhZWU4MTg5ZmUzODc3OGRjIn0.eyJhdWQiOiIyIiwianRpIjoiYjEwNzRlMGU3ODAwMWQ1NWEzOGQwZTk4NmM1N2ZlOTAzY2Q5OWE1NDU3MDE3NmQ3ZjU0MWU5MjljYWRiZDhlZWFlZTgxODlmZTM4Nzc4ZGMiLCJpYXQiOjE1NDk2Mjc3MTgsIm5iZiI6MTU0OTYyNzcxOCwiZXhwIjoxNTgxMTYzNzE4LCJzdWIiOiIxIiwic2NvcGVzIjpbXX0.rx7GLBhZmt8lLI0aEpGSS1gpXVckPDSk9XGG_BuywI7EQfUcY0azrMKQJOCyXOnDc_yPpiX0ivlzPgqNzM7bEQm2LDKOlHnOtCvah2klUnAZpHsaUrKoEpsDgdnVWifJ4KpyjppIaXU7BXIFFObmO35MZDOVX4zDmb2n8FOfiEWM5QSVn85k_Br1xdpegzgAFOJ6HNoi6egB5XJ55cGBC9LZvNnO_woTqnXz7Dntbm2wDQX-cGapv-OCqWHXq8BLFGvDxiQvHjDkjLAjIsl5V57INUbb_tO6z2TttzMgK8nXjrfZjS3JzrMiZOhQrLXeZ4AJ2TO6DAZ-N8m4-oaEc-st-kZrdmi6JmZT0VjkShoXNitV3yQA8r7lZDZ-urwWpGl82-6p0TXqJR707SepEH_2uL2GPpEcwB86qASM0Iko0jIyE-EoVG8rR2nSH7c-sYyc4wsQSDT26phb-I_I_6OXZwxLV-pZa8F_by77DnOgyJ3htgjf0uKERxt8KOIvYyymRjkAHTTqbUIMY4PxuKllgj_XL2hRvbF23bcV-c4y94Jhq3Z-Ojdk6wf-zfN1ErdKUE7KjpWPI3DkrZhCZuNrOcWK2J_MOj_n9Gzt8URJ9i-8mxMvS3C9h60SBR1LUcujXyi6_e_4L4ZSKCPVOl9RKZJOdQgg-JBfNexr_y0 |
Close the window. From the Environment drop down select EAPI
Select the localhost:8000/api/user tab and change the authorisation value to {{ auth }}
key | value | description |
---|---|---|
Accept | application/json | |
Content-Type | application/json | |
Authorization | {{auth}} |
Click send and the user details will be returned.
Click Save ▽ > Save as.. > Get Auth Token
Finally save the lesson to git:
git status
view the output, should be ~20 files, as User Auth has also been created.
git add .
git commit -m "Passport Installed"
[master 338625f] Passport Installed 14 files changed, 900 insertions(+), 24 deletions(-) create mode 100644 app/Http/Controllers/HomeController.php create mode 100644 resources/views/auth/login.blade.php create mode 100644 resources/views/auth/passwords/email.bl create mode 100644 resources/views/auth/passwords/reset.bl create mode 100644 resources/views/auth/register.blade.php create mode 100644 resources/views/auth/verify.blade.php create mode 100644 resources/views/home.blade.php create mode 100644 resources/views/layouts/app.blade.php
git push
Enumerating objects: 42, done. Counting objects: 100% (42/42), done. Delta compression using up to 4 threads Compressing objects: 100% (26/26), done. Writing objects: 100% (27/27), 8.17 KiB | 1.17 MiB/s, done. Total 27 (delta 16), reused 0 (delta 0) remote: Resolving deltas: 100% (16/16), completed with 12 local objects. To https://github.com/Pen-y-Fan/api ab725f5..338625f master -> master
Run route:list:
php artisan route:list
Look for the route for product.store:
Domain | Method | URI | Name | Action | Middleware |
---|---|---|---|---|---|
POST | api/products | products.store | App\Http\Controllers\ProductController@store | api |
We need a POST request to api/products which hits the ProductController store method.
ProductController.php add __constructor for middleware, so any update routes will need to be authenticated
public function __construct()
{
$this->middleware('auth:api')->except('index', 'show');
}
note: only one colon (:) between auth and api (?) Tried with two and it failed :/
Test with Postman to POST to localhost:8000/api/products with headers Accept and Content-Type only. The result will be Unauthenticated.
{
"message": "Unauthenticated."
}
Add Authorization {{auth}} and it will return a blank page, as the store method hasn't been setup, yet.
key | value | description |
---|---|---|
Accept | application/json | |
Content-Type | application/json | |
Authorization | {{auth}} |
Nothing will display as the store method is empty. Back on the ProductController.php edit the store method
public function store(Request $request)
{
return 'Test message';
}
Test with Postman and it now displays 'Test data';
Next we need to make the request for ProductRequest:
php artisan make:request ProductRequest
Request created successfully.
Open App\Http\Requests ProductRequest.php
Edit the Authorise method, return true, then rules
public function authorize()
{
return true;
}
// ...
public function rules()
{
return [
'name' => 'required|max:255|unique:products',
'description' => 'required',
'price' => 'required|max:10',
'stock' => 'required|max:6',
'discount' => 'required|max:2'
];
}
Edit ProductController.php edit the store method change to ProductRequest and import it.
use App\Http\Requests\ProductRequest;
// ..
public function store(ProductRequest $request) // Was Request $request
Post a request using postman for a product, input the following in the Body as raw:
{
"name": "Iphone X",
"description": "The best ever phone with face ID",
"price": "100",
"stock": "10",
"discount": "50"
}
Test by adding return $request->all(); to the store method on the ProductController.php
public function store(ProductRequest $request)
{
return $request->all();
}
The details posted will be returned and displayed in postman.
Next update the store method to take the post and store the new product in the database
public function store(ProductRequest $request)
{
$product = new Product;
$product->name = $request->name;
$product->detail = $request->description;
$product->price = $request->price;
$product->stock = $request->stock;
$product->discount = $request->discount;
$product->save();
return response(
[
'data' => new ProductResource($product)
],
Response::HTTP_CREATED
);
}
Also import the namespace
use Symfony\Component\HttpFoundation\Response;
php artisan route:list
Domain | Method | URI | Name | Action | Middleware |
---|---|---|---|---|---|
GET|HEAD | / | Closure | web | ||
POST | api/products | products.store | App\Http\Controllers\ProductController@store | api,auth:api | |
GET|HEAD | api/products | products.index | App\Http\Controllers\ProductController@index | api | |
GET|HEAD | api/products/{product} | products.show | App\Http\Controllers\ProductController@show | api | |
DELETE | api/products/{product} | products.destroy | App\Http\Controllers\ProductController@destroy | api,auth:api | |
PUT|PATCH | api/products/{product} | products.update | App\Http\Controllers\ProductController@update | api,auth:api | |
POST | api/products/{product}/reviews | reviews.store | App\Http\Controllers\ReviewController@store | api | |
GET|HEAD | api/products/{product}/reviews | reviews.index | App\Http\Controllers\ReviewController@index | api | |
DELETE | api/products/{product}/reviews/{review} | reviews.destroy | App\Http\Controllers\ReviewController@destroy | api | |
PUT|PATCH | api/products/{product}/reviews/{review} | reviews.update | App\Http\Controllers\ReviewController@update | api | |
GET|HEAD | api/products/{product}/reviews/{review} | reviews.show | App\Http\Controllers\ReviewController@show | api | |
GET|HEAD | api/user | Closure | api,auth:api | ||
GET|HEAD | home | home | App\Http\Controllers\HomeController@index | web,auth | |
POST | login | App\Http\Controllers\Auth\LoginController@login | web,guest | ||
GET|HEAD | login | login | App\Http\Controllers\Auth\LoginController@showLoginForm | web,guest | |
POST | logout | logout | App\Http\Controllers\Auth\LoginController@logout | web | |
GET|HEAD | oauth/authorize | passport.authorizations.authorize | Laravel\Passport\Http\Controllers\AuthorizationController@authorize | web,auth | |
DELETE | oauth/authorize | passport.authorizations.deny | Laravel\Passport\Http\Controllers\DenyAuthorizationController@deny | web,auth | |
POST | oauth/authorize | passport.authorizations.approve | Laravel\Passport\Http\Controllers\ApproveAuthorizationController@approve | web,auth | |
POST | oauth/clients | passport.clients.store | Laravel\Passport\Http\Controllers\ClientController@store | web,auth | |
GET|HEAD | oauth/clients | passport.clients.index | Laravel\Passport\Http\Controllers\ClientController@forUser | web,auth | |
DELETE | oauth/clients/{client_id} | passport.clients.destroy | Laravel\Passport\Http\Controllers\ClientController@destroy | web,auth | |
PUT | oauth/clients/{client_id} | passport.clients.update | Laravel\Passport\Http\Controllers\ClientController@update | web,auth | |
POST | oauth/personal-access-tokens | passport.personal.tokens.store | Laravel\Passport\Http\Controllers\PersonalAccessTokenController@store | web,auth | |
GET|HEAD | oauth/personal-access-tokens | passport.personal.tokens.index | Laravel\Passport\Http\Controllers\PersonalAccessTokenController@forUser | web,auth | |
DELETE | oauth/personal-access-tokens/{token_id} | passport.personal.tokens.destroy | Laravel\Passport\Http\Controllers\PersonalAccessTokenController@destroy | web,auth | |
GET|HEAD | oauth/scopes | passport.scopes.index | Laravel\Passport\Http\Controllers\ScopeController@all | web,auth | |
POST | oauth/token | passport.token | Laravel\Passport\Http\Controllers\AccessTokenController@issueToken | throttle | |
POST | oauth/token/refresh | passport.token.refresh | Laravel\Passport\Http\Controllers\TransientTokenController@refresh | web,auth | |
GET|HEAD | oauth/tokens | passport.tokens.index | Laravel\Passport\Http\Controllers\AuthorizedAccessTokenController@forUser | web,auth | |
DELETE | oauth/tokens/{token_id} | passport.tokens.destroy | Laravel\Passport\Http\Controllers\AuthorizedAccessTokenController@destroy | web,auth | |
POST | password/email | password.email | App\Http\Controllers\Auth\ForgotPasswordController@sendResetLinkEmail | web,guest | |
GET|HEAD | password/reset | password.request | App\Http\Controllers\Auth\ForgotPasswordController@showLinkRequestForm | web,guest | |
POST | password/reset | password.update | App\Http\Controllers\Auth\ResetPasswordController@reset | web,guest | |
GET | HEAD | password/reset/{token} | password.reset | App\Http\Controllers\Auth\ResetPasswordController@showResetForm | |
GET|HEAD | register | register | App\Http\Controllers\Auth\RegisterController@showRegistrationForm | web,guest | |
POST | register | App\Http\Controllers\Auth\RegisterController@register | web,guest |
The route for products.update is a PUT|PATCH with a route of api/products/{product} using the controller App\Http\Controllers\ ProductController using the update method.
Check PHPMyAdmin products table and view the last products, 101 is Iphone X, the url required to update this entry is localhost/api/products/101
Edit the ProductsController.php update method:
public function update(Request $request, Product $product)
{
return $request;
}
In postman send the request and the request data will be returned.
On the ProductsController.php change to return $product; (Note not $product->all(); - this returns all products). This proves the request is being matched to the product.
To update the product with the request data change the update method:
public function update(Request $request, Product $product)
{
$product->update($request);
}
Before the product can be updated the Product.php class need to have a $fillable attribute created.
// ...
class Product extends Model
{
protected $fillable = ['name', 'detail', 'stock', 'price', 'discount'];
// ...
In postman copy the product create saved entry, save as product update to the products collection. Modify type from POST to PUT and the url localhost:8000/api/products/101. The headers and body should be the same as used in lesson 13, modify the body to update the name to "IPhone X updated". Clicking Send will show a status of 200 OK, but no return value. Check PHPMyAdmin products table
SELECT * FROM products WHERE id=101;
"101","Iphone X updated","The best ever phone with face ID","100","10","50","2019-02-08 18:25:29","2019-02-11 10:30:13"
Another way is to use Postman to GET localhost:8000/api/products/101
{
"data": {
"name": "Iphone X updated",
"description": "The best ever phone with face ID",
"listPrice": 100,
"discount(%)": 50,
"purchasePrice": 50,
"stock": 10,
"rating": "No rating yet",
"href": {
"reviews": "<http://localhost:8000/api/products/101/reviews>"
}
}
}
If the product description is updated it will not change, as the API is sending the field as description, but the database is detail. Amend the update method to make the detail and same a description, then remove description from the array, then return a response.
public function update(Request $request, Product $product)
{
$request['detail'] = $request->description;
unset($request['description']);
$product->update($request->all());
return response(
[
'data' => new ProductResource($product)
], Response::HTTP_CREATED
);
}
Use postman to PUT an update for product 101 with a description "description": "The best ever phone with face ID updated", now we have a Response:
{
"data": {
"name": "Iphone X updated",
"description": "The best ever phone with face ID updated",
"listPrice": 100,
"discount(%)": 50,
"purchasePrice": 50,
"stock": 10,
"rating": "No rating yet",
"href": {
"reviews": "<http://localhost:8000/api/products/101/reviews>"
}
}
}
git status
... modified: app/Http/Controllers/ProductController.php modified: app/Model/Product.php ...
git add .
git commit -m "Product Update"
[master f32fed2] Product Update 2 files changed, 3 insertions(+), 1 deletion(-)
git push
Enumerating objects: 15, done. Counting objects: 100% (15/15), done. Delta compression using up to 4 threads Compressing objects: 100% (8/8), done. Writing objects: 100% (8/8), 741 bytes | 741.00 KiB/s, done. Total 8 (delta 6), reused 0 (delta 0) remote: Resolving deltas: 100% (6/6), completed with 6 local objects. To https://github.com/Pen-y-Fan/api 55d05fd..f32fed2 master -> master
To find the route to destroy (delete) a product run:
php artisan route:list
the list will be the same as lecture 14 the products.destroy,
Domain | Method | URI | Name | Action | Middleware |
---|---|---|---|---|---|
DELETE | api/products/{product} | products.destroy | App\Http\Controllers\ProductController@destroy | api |
The Method is DELETE with a url of api/products/{product} on the ProductController using the destroy method.
In Postman open EAPI > Product Create, save as Product Destroy in EAPI > Products, amend the method to DELETE, the url will be localhost/api/products/101, remove the Body, the headers will be the same as to create, Content-Type and Accept as JSON and Authorization as {{auth}}
Edit ProductController.php, destroy method, check the route is working by returning the $product:
public function update(Request $request, Product $product)
{
return $product;
}
{
"id": 101,
"name": "Iphone X updated",
"detail": "The best ever phone with face ID updated",
"price": 100,
"stock": 10,
"discount": 50,
"created_at": "2019-02-08 18:25:29",
"updated_at": "2019-02-11 10:49:10"
}
The simplest way to delete is to change the return to $product->delete();
public function update(Request $request, Product $product)
{
$product->delete();
}
This time Postman will return with status 200 OK.
Double check by running Postman to GET product 101:
"message": "No query results for model [App\\Model\\Product].",
....
Also PHPMyAdmin SELECT * FROM products WHERE ID=101; returns nothing.
To provide a Response, just return null, HTTP_NO_CONTENT (204).
public function destroy(Product $product)
{
$product->delete();
return response(null, Response::HTTP_NO_CONTENT);
}
Use Postman to DELETE record 102, the return status code is now 204 No Content.
To check another product, that has a review, GET product 38 and check the reviews.
Then click the reviews link.
{
"data": [
{
"customer": "Marques King",
"body": "Aut animi fugit sed omnis quod perspiciatis. Corrupti aspernatur quibusdam sint dolorem necessitatibus. Aut eaque libero nihil enim est enim. Odio amet veritatis in vel.",
"star": 4
},
Using PHPMyAdmin check the reviews by customer Marques King.
SELECT * FROM reviews WHERE customer = 'Marques King';
One review id 30.
SELECT count(id) FROM reviews WHERE product_id = 38;
count(id) 6
Now use Postman to delete product 38 (localhost:8000/api/products/38).
204 No Content
Recheck the reviews by Marques King and the number of reviews for product_id 38:
No review and count(id) 0.
So deleting worked and cascading the reviews
git add .
git commit -m "Product destroy"
git push
If a product GET for a product that doesn't exist an error page will be displayed, same for updating a product.
Open App>Exceptions> Handler.php Edit the render method to die and dump the $exception
public function render($request, Exception $exception)
{
dd($exception);
return parent::render($request, $exception);
}
Now in Postman try to GET product 101 (which was deleted last lesson)
Preview the Body for the return.
ModelNotFoundException ...
Write an if statement to catch the exception, also add an if statement to return a JSON message, 'Model not found', so users with an API, who expect a JSON Response, call will get the message, but an website url will get the full error message for debugging.
// ...
use Exception;
use Symfony\Component\HttpKernel\Exception\NotFoundHttpException; // Added
use Illuminate\Database\Eloquent\ModelNotFoundException; // Added
use Symfony\Component\HttpFoundation\Response; // Added
use Illuminate\Foundation\Exceptions\Handler as ExceptionHandler;
// ...
public function render($request, Exception $exception)
{
if ($request->expectsJson()) {
if ($exception instanceof ModelNotFoundException) {
return response()->json(
[
'errors' => 'Product Model not found'
],
Response::HTTP_NOT_FOUND
);
}
}
return parent::render($request, $exception);
}
}
Use Postman to try to GET product 101
"Model not found"
View the url in chrome and and full exception is displayed.
What happens if the url is wrong?
In postman try to GET product 100 with the url localhost:8000/api/products/100
404 Not found is returned. NotFoundHttpException
public function render($request, Exception $exception)
..
if ($exception instanceof NotFoundHttpException) {
return response()->json(
[
'errors' => 'Incorrect path'
],
Response::HTTP_NOT_FOUND
);
}
return parent::render($request, $exception);
}
End of lesson, more refactoring to come next lesson, commit this lesson to Git.
git status
git add .
git commit -m "Handles the exceptions"
git push
Split the exception handler into a new file called ExceptionTrait.php, in the same app \ Exceptions folder, Move the use code from the Handler.php, move the inner if statements and create a new trait
<?php
namespace App\Exceptions;
use Illuminate\Database\Eloquent\ModelNotFoundException;
use Symfony\Component\HttpKernel\Exception\NotFoundHttpException;
use Symfony\Component\HttpFoundation\Response;
trait ExceptionTrait
{
public function apiException($request, $e)
{
if ($e instanceof ModelNotFoundException) {
return response()->json(
[
'errors' => 'Product Model not found'
],
Response::HTTP_NOT_FOUND
);
}
if ($e instanceof NotFoundHttpException) {
return response()->json(
[
'errors' => 'Incorrect path'
],
Response::HTTP_NOT_FOUND
);
}
}
}
Update the render model to call the apiException when the request expectsJson.
public function render($request, Exception $exception)
{
if ($request->expectsJson()) {
return $this->apiException($request, $exception);
}
return parent::render($request, $exception);
}
Use Postman to test, for incorrect product and url.
Refactor the ExceptionTrait once more, to add functions for isModel and isHtml, then split the Responses to httpResponse and modelResponse
<?php
namespace App\Exceptions;
use Symfony\Component\HttpKernel\Exception\NotFoundHttpException;
use Illuminate\Database\Eloquent\ModelNotFoundException;
use Symfony\Component\HttpFoundation\Response;
trait ExceptionTrait
{
public function apiException($request,$e)
{
if ($this->isModel($e)) {
return $this->ModelResponse($e);
}
if ($this->isHttp($e)) {
return $this->HttpResponse($e);
}
return parent::render($request, $e);
}
protected function isModel($e)
{
return $e instanceof ModelNotFoundException;
}
protected function isHttp($e)
{
return $e instanceof NotFoundHttpException;
}
protected function ModelResponse($e)
{
return response()->json(
[
'errors' => 'Product Model not found'
],
Response::HTTP_NOT_FOUND
);
}
protected function HttpResponse($e)
{
return response()->json(
[
'errors' => 'Incorrect path'
],
Response::HTTP_NOT_FOUND
);
}
}
git status
git add .
git commit -m "Customised Exception Trait"
git push
Authorise for Update and destroy of the product, currently anyone can update and delete any products, this needs to be restricted so users can only update their own products. In the database > products table there is no field for user ID. Edit database > migrations > 2019_02_06_154700_create_products_table.php. Amend the up method, add the field user_id
public function up()
{
Schema::create(
'products', function (Blueprint $table) {
$table->increments('id');
$table->string('name');
$table->text('detail');
$table->integer('price');
$table->integer('stock');
$table->integer('discount');
$table->integer('user_id')->unsigned()->index(); // Added
$table->timestamps();
}
);
}
Now refresh all the databases:
php artisan migrate:refresh
Rolling back: 2016_06_01_000005_create_oauth_personal_access_clients_table Rolled back: 2016_06_01_000005_create_oauth_personal_access_clients_table Rolling back: 2016_06_01_000004_create_oauth_clients_table Rolled back: 2016_06_01_000004_create_oauth_clients_table Rolling back: 2016_06_01_000003_create_oauth_refresh_tokens_table Rolled back: 2016_06_01_000003_create_oauth_refresh_tokens_table Rolling back: 2016_06_01_000002_create_oauth_access_tokens_table Rolled back: 2016_06_01_000002_create_oauth_access_tokens_table Rolling back: 2016_06_01_000001_create_oauth_auth_codes_table Rolled back: 2016_06_01_000001_create_oauth_auth_codes_table Rolling back: 2019_02_06_154851_create_reviews_table Rolled back: 2019_02_06_154851_create_reviews_table Rolling back: 2019_02_06_154700_create_products_table Rolled back: 2019_02_06_154700_create_products_table Rolling back: 2014_10_12_100000_create_password_resets_table Rolled back: 2014_10_12_100000_create_password_resets_table Rolling back: 2014_10_12_000000_create_users_table Rolled back: 2014_10_12_000000_create_users_table Migrating: 2014_10_12_000000_create_users_table Migrated: 2014_10_12_000000_create_users_table Migrating: 2014_10_12_100000_create_password_resets_table Migrated: 2014_10_12_100000_create_password_resets_table Migrating: 2016_06_01_000001_create_oauth_auth_codes_table Migrated: 2016_06_01_000001_create_oauth_auth_codes_table Migrating: 2016_06_01_000002_create_oauth_access_tokens_table Migrated: 2016_06_01_000002_create_oauth_access_tokens_table Migrating: 2016_06_01_000003_create_oauth_refresh_tokens_table Migrated: 2016_06_01_000003_create_oauth_refresh_tokens_table Migrating: 2016_06_01_000004_create_oauth_clients_table Migrated: 2016_06_01_000004_create_oauth_clients_table Migrating: 2016_06_01_000005_create_oauth_personal_access_clients_table Migrated: 2016_06_01_000005_create_oauth_personal_access_clients_table Migrating: 2019_02_06_154700_create_products_table Migrated: 2019_02_06_154700_create_products_table Migrating: 2019_02_06_154851_create_reviews_table Migrated: 2019_02_06_154851_create_reviews_table
The UserFactory.php is automatically created. Next update the ModelProductFactory.php, add the user_id field with a function to return a random user.
$factory->define(App\Model\Product::class, function (Faker $faker) {
return [
'name' => $faker->word,
'detail' => $faker->paragraph,
'price' => $faker->numberBetween(100, 1000),
'stock' => $faker->randomDigit,
'discount' => $faker->numberBetween(2, 30),
'user_id' => function () { //
return App\User::all()->random(); // Added
}, //
];
});
The DatabaseSeeder.php needs to be updated to call the factory that will create 10 users.
public function run()
{
factory(App\User::class, 10)->create(); // Adde
factory(App\Model\Product::class, 50)->create();
factory(App\Model\Review::class, 300)->create();
}
Note: the order the databases are seeded is important as Products need users and Reviews need Products. Next seed the databases
php artisan db:seed
Database seeding completed successfully.
View the databases in PHPMyAdmin to see there are 10 users, 50 products and 300 reviews.
The ProductController.php need to be updated to check the user id when the user tried to amened or delete a product.
public function update(Request $request, Product $product)
{
$this->ProductUserCheck($product); // Added
$request['detail'] = $request->description;
// ...
public function destroy(Product $product)
{
$this->ProductUserCheck($product); // Added
$product->delete();
// ...
public function ProductUserCheck($product)
{
if (Auth::id() != $product->user_id) {
throw new ProductNotBelongsToUser();
}
}
The exception need to be created, using php artisan:
php artisan make:exception ProductNotBelongsToUser
Exception created successfully.
Open App\Exceptions \ ProductNotBelongsToUser.php
Add a render() function Note: the render function will automatically run:
public function render()
{
return ['errors' => 'This Product does not belong to the User'];
}
The databases have been recreated, except for the passport.
php artisan passport:install
Encryption keys already exist. Use the --force option to overwrite them. Personal access client created successfully. Client ID: 1 Client secret: dAWekH3EZsvRvSa0X2q5Jbphe5sCQalWR6xxn5Qf Password grant client created successfully. Client ID: 2 Client secret: 0Slhmc2hRy88dSAvr6qLmtZZez9wxWOpF9OiN5zz
Use the new token in {{auth}} for Postman.
Repeat the Lesson above to get the token using Postman, POST to localhost:8000/oauth/token with Body of:
{
"grant_type": "password",
"client_id": "2",
"client_secret": "0Slhmc2hRy88dSAvr6qLmtZZez9wxWOpF9OiN5zz",
"username": "[email protected]",
"password": "secret"
}
This will associate the 5th user (see the user table for the email), the password "secret" is created by the user factory.
{
"token_type": "Bearer",
"expires_in": 31536000,
"access_token": "eyJ0eXAiOiJKV1QiLCJhbGciOiJSUzI1NiIsImp0aSI6ImE5ZTE5Y2JjMjAwMjA4YjcxYWFkZGI4OGQ2NWQ1NmVjYzVjMGI1Mzk3N2RmZWMxNDU4MmRlN2QyYjMxM2E5Y2EwMzU5Yjc3MjEyNTYyOWUwIn0.eyJhdWQiOiIyIiwianRpIjoiYTllMTljYmMyMDAyMDhiNzFhYWRkYjg4ZDY1ZDU2ZWNjNWMwYjUzOTc3ZGZlYzE0NTgyZGU3ZDJiMzEzYTljYTAzNTliNzcyMTI1NjI5ZTAiLCJpYXQiOjE1NDk5MDAxODUsIm5iZiI6MTU0OTkwMDE4NSwiZXhwIjoxNTgxNDM2MTg1LCJzdWIiOiI1Iiwic2NvcGVzIjpbXX0.ixVtt0yWrcU0peLLCBCQWxqzSrZZLXBqq85JhCkydAQgJ0TcIc8E0BpyK9_ADTdXokyCV_zSu0TW0Sh9poYB4xWJe-GL6dtOR3UiG6sYQvlpKBCIj6P1d-GDHDcr3Af8aKwJsaGG_2_iOJ6_sY216XtjLORllvNuGNzU8mc69vwh9HSEij4o4iVc2YDExc6fpPaxqkV0AuZ5ibQgFzV0YFGaxW72CMDRpjcOSTROVLt_2Nut-cuUyhVLftiTdsjtJt92stc64rLDCOZ8bLRmJe_sttI2_VZ77QEAeQIDux_NSzzH_KczyAsg1uGT56q907OvVY0F6gMa_MSPvZusb5DauqfCIieiRn0dbLTAy81QMCgtAFctXO4xXpOH7xd5jyGhRIpIHePLCbvwVM_oBw-q_fmfwPAOL8Xd4xIgH4BOp9nHp8jEHpaIr5BQnnftooMWSOvEj10uc1MS9UpKek5wmgjYQ6iesNh3C1rJ_0LPylKucKFXnEMjNMXuby6i_fSTKLFpnyK7nIGRMDWQwpkjGagJj8pUEDOakCwinx0aOcjcR8xro7nu4I_UUrXe8vil05T42Mebe7_-CaaUbS5FgE29xHCSBN3m1eKSn66lmGixEzk0770OoF09gWTOLVS0sLTuMiYmgwwvs5yIfhlXmognuBJrDWG0j2VZfGU",
"refresh_token": "def50200d0b181239188f2ef80d5e495bf3057b90b47bd32bb56f631a1d4ef025e9f495a9f70e3d9164ce2c769b44679685df796d7c3fe9cbf31f2404b7173a6ef3181d8ccb395e4a4d9de39903e69def0c6fe447c9d6c6ebdad87328ff295d4edf50e8f22f118a1913d3f853960a7391d0893a0bf6463d3b35d249621af0c4b27512ef02f9c199f2be7410465f20cab272b2ecdd083c8a3f68bdf94af843890abdd2e350f9cd0fec4406732ecdcdbba706ef7f4eb4ad07e4b41e57c4a7f4b39af8c81b406186522e4c4ad575e989ddf6b618a5adbae0066bd66ca1c4e1018c2afb49098ab60714bd8a256cf9cc959a62a6be97b786fc829a205016c9710fd0e0afc5377d6bf4497497a4a5910301ef6763399ab32c2f809953610bf396360ae2f8df65942ba034354c0af8c2783c5f1b31db12e672894adf7a53c474e1dd6ab1c9ab1336f5721d9592dccf4e273d5ca119ce75697400c306cae40d131664eaff4"
}
Copy the token and Manage settings (under the cog) type Bearer then paste in the token.
Check the Product table for user 5, try to update a product that doesn't belong to user_id 5, the message will be
"errors" : "This Product does not belong to the User."
Then check a product that does belong to the user, the full product will be returned. Tyy and delete a product that belongs to the user. Status 204 No content will be returned.
git status
git add .
git commit -m "Product Does Not Belong To User"
git push
In this lesson we had one store a review for a particular product.
http\Controller\ ReviewController.php has already been setup. So see the routes use:
php artisan route:list
Domain | Method | URI | Name | Action | Middleware |
---|---|---|---|---|---|
... | |||||
POST | api/products/{product}/reviews | reviews.store | App\Http\Controllers\ReviewController@store | api | |
GET|HEAD | api/products/{product}/reviews | reviews.index | App\Http\Controllers\ReviewController@index | api | |
DELETE | api/products/{product}/reviews/{review} | reviews.destroy | App\Http\Controllers\ReviewController@destroy | api | |
PUT|PATCH | api/products/{product}/reviews/{review} | reviews.update | App\Http\Controllers\ReviewController@update | api | |
GET|HEAD | api/products/{product}/reviews/{review} | reviews.show | App\Http\Controllers\ReviewController@show | api | |
... |
Update te store method:
public function store(Request $request,Product $product)
{
return $product
In Postman try to POST a review to localhost:8000/api/products/38/reviews
Product 38 will be returned.
Next create a ReviewRequest
php artisan make:request ReviewRequest
Request created successfully.
Open app\Http\Requests\ ReviewRequest.php, change authorize to true and update the rules.
public function authorize()
{
return true;
}
// ...
public function rules()
{
return [
'customer' => 'required',
'star' => 'required|integer|between:0,5',
'review' => 'required'
];
}
Change the ReviewController.php from Request $request to ReviewRequest $request and use the Class.
use App\Http\Requests\ReviewRequest;
// ...
public function store(ReviewRequest $request,Product $product)
return $product
// ...
Rerun Postman again to the same url and this time errors will display:
{
"message": "The given data was invalid.",
"errors": {
"customer": ["The customer field is required."],
"star": ["The star field is required."],
"review": ["The review field is required."]
}
}
This means it is working.
public function store(ReviewRequest $request, Product $product)
{
$review = new Review($request->all());
$product->reviews()->save($review);
return response(
[
'data' => new ReviewResource($review),
],
Response::HTTP_CREATED
);
Now in postman create a new POST to localhost:8000/api/products/14/reviews, with the same headers as before:
Accept:application/json
Content-Type:application/json
Authorization:{{auth}}
Body of the review:
{
"customer": "Fred Bloggs",
"star": 4,
"review": "Best thing ever"
}
An errors is received for MassAssignmentException. Open Review.php and add a protected $fillable property:
class Review extends Model
{
protected $fillable = [
'star', 'customer', 'review',
];
public function product()
{
return $this->belongsTo(Product::class);
}
}
Try Postman once more and the review will be posted (localhost:8000/api/products/14/reviews).
{
"data": {
"customer": "Fred Bloggs",
"body": "Best thing ever",
"star": 4
}
}
Check Product 14 with Postman. GET Review Show (http://localhost:8000/api/products/14/reviews)
{
...
{
"customer": "Ethel Fahey",
"body": "Nam exercitationem id laboriosam nulla. Est et libero voluptas deleniti vel repellat. Illum delectus esse accusantium eaque aut quis culpa distinctio. Est reprehenderit cumque enim aut similique inventore.",
"star": 2
},
{
"customer": "Fred Bloggs",
"body": "Best thing ever",
"star": 4
}
]
}
git status
git add .
git commit -m "Review Created"
git push
This lesson wil be update and destroy for review. This has been shown previously so will be quickly created.
In Postman copy Review Create and call it Review Update.
As we need to Review ID edit ReviewResource.php and add the id field:
public function toArray($request)
{
return [
'id' => $this->id,
'customer' => $this->customer,
'body' => $this->review,
'star' => $this->star,
];
}
Run postman to view the Reviews to product 14, this time the review id will also be displayed.
{
"data": [
{
//...
}
{
"id": 301,
"customer": "Fred Bloggs",
"body": "Best thing ever",
"star": 4
}
Update the store method on the ReviewController.php
public function store(ReviewRequest $request,Product $product)
{
return $product
}
In Postman create an update request (PUT) for localhost:8000/api/products/14/reviews/301
Headers as before:
Accept:application/json
Content-Type:application/json
Authorization:{{auth}}
Body of the review can be empty, Send, the reply is the review created last lesson:
{
"id": 301,
"product_id": 14,
"customer": "Fred Bloggs",
"review": "Best thing ever",
"star": 4,
"created_at": "2019-02-11 17:18:10",
"updated_at": "2019-02-11 17:18:10"
}
Amend the ReviewController.php
public function store(ReviewRequest $request,Product $product)
{
$review = new Review($request->all());
$product->reviews()->save($review);
return response(
[
'data' => new ReviewResource($review)
],
Response::HTTP_CREATED
);
}
Post same update request, but this time with a body and the existing review will be updated.
{
"customer": "Fred Bloggs",
"star": 4,
"review": "Best thing ever!!"
}
Return data will confirm the review text has been updated:
{
"data": {
"id": 301,
"customer": "Fred Bloggs",
"body": "Best thing ever!!",
"star": 4
}
}
Next in Postman duplicate the Review Update and call it Review Delete, change to to a DELETE request and Save.
No body is required, header as before with authorization.
Send it will create an error, update the destroy method in the ReviewController.php, add Product $product then return $product.
public function destroy(Product $product,Review $review)
{
return $product;
}
Send with Postman once more and the product (14) will be returned
{
"id": 14,
"name": "voluptatibus",
"detail": "Dolorum culpa quia repellat est et qui ipsam dolorum. Omnis dolorem minus nam dolore. Odio rerum excepturi ex. Dolorem animi ut facilis aut magni.",
"price": 904,
"stock": 8,
"discount": 26,
"user_id": 5,
"created_at": "2019-02-11 15:19:14",
"updated_at": "2019-02-11 15:19:14"
}
This proves the route is working. Next update the destroy method in the ReviewController.php.
public function destroy(Product $product, Review $review)
{
$review->delete();
return response(null, Response::HTTP_NO_CONTENT);
}
In postman send once more, Status: 204 No Content. The review has been deleted.
git status
git add .
git commit -m "Update and Destroy Reviews Created - Final!"
git push