Skip to content

Instantly share code, notes, and snippets.

@Pen-y-Fan
Created August 4, 2019 12:30
Show Gist options
  • Star 2 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save Pen-y-Fan/13c957b684f5887470a30abad7913c3d to your computer and use it in GitHub Desktop.
Save Pen-y-Fan/13c957b684f5887470a30abad7913c3d to your computer and use it in GitHub Desktop.
Laravel E-Commerce Restful API

Laravel E-Commerce Restful API

uDemy

The source code is on github bitfumes/api

Notes & disclaimer

  • 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";
\\ ...

Lecture 4

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

my git hub

git remote add api <https://github.com/Pen-y-Fan/api>
git push api

Lecture 5

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

Lecture 6 Database seeding

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.

Lecture 7 Creating Relationship (04:24)

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.

Lecture 8 Create API Resource /Transformer (09:33)

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.

Lecture 9 Transforming Products (09:27)

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

reviews "http://127.0.0.1:8000/api/products/4/reviews"

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.

Lecture 10 Product Collection Transforming (07:01)

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.

Lecture 11 Get Review API Resource (09:20)

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.

Lecture 12 Configure Passport Package (08:49)

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 Mickey@mouse.test
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": "Mickey@mouse.test",
  "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": "Mickey@mouse.test",
  "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

Lecture 13 Create New Product (13:38)

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;

Lecture 14 Update Product Details (08:03)

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

Lecture 15 Destroy a Product (05:45)

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

Lecture 16 Handle Exceptions (08:24)

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

Lecture 17 Custom Exceptions (08:29)

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

Lecture 18 Authorise Product Updates (14:27)

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": "koelpin.hailee@example.net",
  "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

Lecture 19 Create Reviews for Product (09:20)

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

Lecture 20 Complete Review (08:49)

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
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment