app
   +---- Console
   |      +---- Commands
   |      +---- Kernel.php

    <?php

    namespace App\Console;

    use Illuminate\Console\Scheduling\Schedule;
    use Laravel\Lumen\Console\Kernel as ConsoleKernel;

    class Kernel extends ConsoleKernel
    {
        /**
        * The Artisan commands provided by your application.
        *
        * @var array
        */
        protected $commands = [
            //
        ];

        /**
        * Define the application's command schedule.
        *
        * @param  \Illuminate\Console\Scheduling\Schedule  $schedule
        * @return void
        */
        protected function schedule(Schedule $schedule)
        {
            //
        }
    }
    
   +---- Events
   |      +---- Event.php

    <?php

    namespace App\Events;

    use Illuminate\Queue\SerializesModels;

    abstract class Event
    {
        use SerializesModels;
    }
    
   |      +---- ExampleEvent.php

    <?php

    namespace App\Events;

    class ExampleEvent extends Event
    {
        /**
        * Create a new event instance.
        *
        * @return void
        */
        public function __construct()
        {
            //
        }
    }
    
   +---- Exceptions
   |      +---- Handler.php

    <?php

    namespace App\Exceptions;

    use Exception;
    use Symfony\Component\HttpFoundation\Response;
    use Illuminate\Validation\ValidationException;
    use Illuminate\Auth\Access\AuthorizationException;
    use Illuminate\Http\Exceptions\HttpResponseException;
    use Illuminate\Database\Eloquent\ModelNotFoundException;
    use Symfony\Component\HttpKernel\Exception\HttpException;
    use Laravel\Lumen\Exceptions\Handler as ExceptionHandler;

    class Handler extends ExceptionHandler
    {
        /**
        * A list of the exception types that should not be reported.
        *
        * @var array
        */
        protected $dontReport = [
            AuthorizationException::class,
            HttpException::class,
            ModelNotFoundException::class,
            ValidationException::class,
        ];

        /**
        * Report or log an exception.
        *
        * This is a great spot to send exceptions to Sentry, Bugsnag, etc.
        *
        * @param  \Exception  $e
        * @throws \Exception
        * @return void
        */
        public function report(Exception $e)
        {
            parent::report($e);
        }

        /**
        * Render an exception into an HTTP response.
        *
        * @param  \Illuminate\Http\Request  $request
        * @param  \Exception  $e
        * @return \Illuminate\Http\Response
        */
        public function render($request, Exception $e)
        {
            if ($e instanceof HttpException) {
                $message = $e->getMessage() ?: Response::$statusTexts[$e->getStatusCode()];

                return response()->json((
                    ['errors' => [
                        // 'status' => $e->getStatusCode(),
                        'message' => $message,
                    ]
                    ]), $e->getStatusCode());
            }

            if ($e instanceof ValidationException) {
                $formattedErrors = [];
                foreach ($e->validator->errors()->getMessages() as $key => $messages) {
                    $key = preg_replace('/[a-z]+\./', '', $key);
                    $formattedErrors[$key] = array_map(function ($msg) {
                        return preg_replace('/The [a-zA-Z ]+\.([a-z]+) /', '', $msg);
                    }, $messages);
                }
                return response()->json(['errors' => $formattedErrors], 422);
            }

            if ($e instanceof Exception && ! env('APP_DEBUG')) {
                return response()->json([
                    'errors' => [
                        class_basename($e) => $e->getMessage()
                    ]
                ], 500);
            }
        }
    }
    
   +---- Http
   |      +---- Controllers
   |            +---- ArticleController.php

    <?php

    namespace App\Http\Controllers;

    use Auth;
    use App\Models\Tag;
    use App\Models\Article;
    use Illuminate\Http\Request;
    use App\Http\Resources\ArticleResource;
    use App\RealWorld\Filters\ArticleFilter;
    use App\Http\Controllers\Concerns\GetsArticles;
    use App\Http\Validators\ValidatesArticleRequests;

    class ArticleController extends Controller
    {
        use GetsArticles, ValidatesArticleRequests;

        /**
        * ArticleController constructor.
        *
        * @param ArticleFilter $filter
        */
        public function __construct()
        {
            $this->middleware('auth', ['except' => [
                'index',
                'show',
                'tags'
            ]]);
            $this->middleware('auth:optional', ['only' => [
                'index',
                'show'
            ]]);
        }

        /**
        * Get all the articles.
        *
        * @return \Illuminate\Http\Response
        */
        public function index(ArticleFilter $filter)
        {
            $articles = $this->paginate(Article::filter($filter));
            return ArticleResource::collection($articles);
        }

        /**
        * Create a new article and return the article if successful.
        *
        * @param  \Illuminate\Http\Request  $request
        * @return \Illuminate\Http\Response
        */
        public function store(Request $request)
        {
            $this->validateNew($request);

            $article = Article::create([
                'title' => $request->input('article.title'),
                'description' => $request->input('article.description'),
                'body' => $request->input('article.body'),
            ]);

            Auth::user()->articles()->save($article);

            $inputTags = $request->input('article.tagList');

            if ($inputTags && ! empty($inputTags)) {
                foreach ($inputTags as $name) {
                    $article->tags()->attach(new Tag(['name' => $name]));
                }
            }
            return (new ArticleResource($article))
                    ->response()
                    ->header('Status', 201);
        }

        /**
        * Get the article given by its slug.
        *
        * @param  string  $slug
        * @return \Illuminate\Http\Response
        */
        public function show(string $slug)
        {
            $article = $this->getArticleBySlug($slug);
            return new ArticleResource($article);
        }

        /**
        * Update the article given by its slug and return the article if successful.
        *
        * @param  \Illuminate\Http\Request  $request
        * @param  string  $slug
        * @return \Illuminate\Http\Response
        */
        public function update(Request $request, string $slug)
        {
            $this->validateUpdate($request);

            if ($request->has('article')) {
                $article = $this->getArticleBySlug($slug);
                if ($request->user()->cannot('update-article', $article)) {
                    abort(401);
                }
                $article->update($request->get('article'));
            }
            return new ArticleResource($article);
        }

        /**
        * Remove the specified resource from storage.
        *
        * @param  \Illuminate\Http\Request  $request
        * @param  string  $slug
        * @return \Illuminate\Http\Response
        */
        public function destroy(Request $request, string $slug)
        {
            $article = $this->getArticleBySlug($slug);
            if ($request->user()->cannot('delete-article', $article)) {
                abort(403);
            }

            $article->delete();
            return $this->respondSuccess();
        }

        /**
        * Get all the articles of users that are followed by the authenticated user.
        *
        * @return \Illuminate\Http\Response
        */
        public function feed()
        {
            $articles = $this->paginate(Auth::user()->feed());
            return ArticleResource::collection($articles);
        }

        /**
        * Favorite the article given by its slug and return the article if successful.
        *
        * @param  \Illuminate\Http\Request  $request
        * @param  string  $slug
        * @return \Illuminate\Http\JsonResponse
        */
        public function addFavorite(Request $request, string $slug)
        {
            $article = $this->getArticleBySlug($slug);
            if ($request->user()->can('favorite-article', $article)) {
                $request->user()->favorite($article);
            }
            return new ArticleResource($article);
        }

        /**
        * Unfavorite the article given by its slug and return the article if successful.
        *
        * @param  string  $slug
        * @return \Illuminate\Http\JsonResponse
        */
        public function unFavorite(string $slug)
        {
            $article = $this->getArticleBySlug($slug);
            Auth::user()->unFavorite($article);
            $article->save();

            return new ArticleResource($article);
        }

        /**
        * Get all the tags.
        *
        * @return \Illuminate\Http\JsonResponse
        */
        public function tags()
        {
            $names = Article::distinct('tags')->get()->pluck('name');
            $tags = $names->unique()->sort()->values()->all();

            return $this->respond(['tags' => $tags]);
        }
    }
    
   |            +---- AuthController.php

    <?php

    namespace App\Http\Controllers;

    use Auth;
    use App\Models\User;
    use Illuminate\Http\Request;
    use App\Http\Resources\UserResource;
    use Illuminate\Support\Facades\Hash;
    use App\Http\Validators\ValidatesAuthenticationRequests;

    class AuthController extends Controller
    {
        use ValidatesAuthenticationRequests;

        /**
        * Login user and return the user is successful.
        *
        * @param Request $request
        * @return \Illuminate\Http\JsonResponse
        */
        public function login(Request $request)
        {
            $this->validateLogin($request);

            $credentials = $request->all()['user'];

            if (! Auth::once($credentials)) {
                return $this->respondFailedLogin();
            }

            return new UserResource(Auth::user());
        }

        /**
        * Register a new user and return the user if successful.
        *
        * @param Request $request
        * @return \Illuminate\Http\JsonResponse
        */
        public function register(Request $request)
        {
            $this->validateRegister($request);

            $user = User::create([
                'username' => $request->input('user.username'),
                'email' => $request->input('user.email'),
                'password' => Hash::make($request->input('user.password')),
            ]);

            return (new UserResource($user))->response()->header('Status', 201);
        }

        /**
        * Respond with failed login.
        *
        * @return \Illuminate\Http\JsonResponse
        */
        protected function respondFailedLogin()
        {
            return $this->respond([
                'errors' => [
                    'email or password' => ['is invalid'],
                ]
            ], 422);
        }
    }
    
   |            +---- CommentController.php

    <?php

    namespace App\Http\Controllers;

    use Auth;
    use Illuminate\Http\Request;
    use App\Http\Resources\CommentResource;
    use App\Http\Controllers\Concerns\GetsArticles;

    class CommentController extends Controller
    {
        use GetsArticles;

        /**
        * CommentController constructor.
        *
        */
        public function __construct()
        {
            $this->middleware('auth', ['except' => 'index']);
            $this->middleware('auth:optional', ['only' => 'index']);
        }

        /**
        * Display a listing of the resource.
        *
        * @param  string  $slug
        * @return \Illuminate\Http\Response
        */
        public function index(string $slug)
        {
            $article = $this->getArticleBySlug($slug);
            return CommentResource::collection($article->comments);
        }

        /**
        * Store a newly created resource in storage.
        *
        * @param  \Illuminate\Http\Request  $request
        * @param  string  $slug
        * @return \Illuminate\Http\Response
        */
        public function store(Request $request, string $slug)
        {
            $this->validate($request, [
                'comment.body' => 'required|string',
            ]);

            $article = $this->getArticleBySlug($slug);

            $article->comments()->create([
                'body' => $request->input('comment.body'),
                'author_id' => Auth::user()->id,
            ]);
            $comment = $article->comments->pop();

            return (new CommentResource($comment))->response()->header('Status', 201);
        }

        /**
        * Remove the specified resource from storage.
        *
        * @param  \Illuminate\Http\Request  $request
        * @param  string  $slug
        * @param  string  $id
        * @return \Illuminate\Http\Response
        */
        public function destroy(Request $request, string $slug, string $id)
        {
            $article = $this->getArticleBySlug($slug);
            $comment = $article->comments()->firstWhere('id', $id);

            if ($request->user()->cannot('delete-comment', $comment)) {
                abort(401);
            }

            if ($comment->delete()) {
                return $this->respondSuccess();
            }
        }
    }
    
   |            +---- Concerns
   |                  +---- GetsArticles.php

    <?php

    namespace App\Http\Controllers\Concerns;

    use App\Models\Article;

    trait GetsArticles
    {
        /**
        * Retrieve article by its slug
        * @param  string $slug
        * @return \App\Models\Article
        */
        protected function getArticleBySlug(string $slug)
        {
            return Article::where('slug', $slug)->firstOrFail();
        }
    }
    
   |            +---- Controller.php

    <?php

    namespace App\Http\Controllers;

    use Illuminate\Support\Collection;
    use Laravel\Lumen\Routing\Controller as BaseController;

    class Controller extends BaseController
    {
        /**
        * Return generic json response with the given data.
        *
        * @param $data
        * @param int $statusCode
        * @param array $headers
        * @return \Illuminate\Http\JsonResponse
        */
        protected function respond($data, $statusCode = 200, $headers = [])
        {
            return response($data, $statusCode, $headers);
        }

        /**
        * Respond with success.
        *
        * @return \Illuminate\Http\JsonResponse
        */
        protected function respondSuccess()
        {
            return $this->respond(null, 204);
        }

        /**
        * Paginate and filter a collection of items
        *
        * @param Collection $collection
        * @param int $offset
        * @return Collection
        */
        protected function paginate(Collection $collection, $offset = 0)
        {
            if (sizeof($collection)) {
                $offset = app('request')->get('offset', $offset);
                $limit = app('request')->get('limit', $collection->first()->getPerPage());

                if (app('request')->has('offset')) {
                    $collection = $collection->slice($offset, $limit)->values();
                }
            }
            return $collection;
        }
    }
    
   |            +---- ProfileController.php

    <?php

    namespace App\Http\Controllers;

    use Auth;
    use App\Models\User;
    use App\Http\Resources\ProfileResource;

    class ProfileController extends Controller
    {

        /**
        * ProfileController constructor.
        *
        */
        public function __construct()
        {
            $this->middleware('auth', ['except' => 'show']);
            $this->middleware('auth:optional', ['only' => 'show']);
        }

        /**
        * Get the profile of the user given by their username
        *
        * @param string $username
        * @return \Illuminate\Http\Response
        */
        public function show($username)
        {
            $user = $this->getUserByName($username);
            return new ProfileResource($user);
        }

        /**
        * Follow the user given by their username and return the user if successful.
        *
        * @param string $username
        * @return \Illuminate\Http\JsonResponse
        */
        public function follow(string $username)
        {
            $user = $this->getUserByName($username);
            Auth::user()->follow($user);
            return new ProfileResource($user);
        }

        /**
        * Unfollow the user given by their username and return the user if successful.
        *
        * @param string $username
        * @return \Illuminate\Http\JsonResponse
        */
        public function unFollow(string $username)
        {
            $user = $this->getUserByName($username);
            Auth::user()->unFollow($user);
            return new ProfileResource($user);
        }

        /**
        * Retrieve user by their username
        * @param  string $username
        * @return \App\Models\User
        */
        protected function getUserByName(string $username)
        {
            if (! $user = User::whereUsername($username)->first()) {
                abort(404, "User ${username} not found");
            }
            return $user;
        }
    }
    
   |            +---- UserController.php

    <?php

    namespace App\Http\Controllers;

    use Auth;
    use Illuminate\Http\Request;
    use App\Http\Resources\UserResource;
    use App\Http\Validators\ValidatesUserRequests;

    class UserController extends Controller
    {
        use ValidatesUserRequests;

        /**
        * UserController constructor.
        *
        */
        public function __construct()
        {
            $this->middleware('auth');
        }

        /**
        * Get the authenticated user.
        *
        * @return \Illuminate\Http\Response
        */
        public function index()
        {
            return new UserResource(Auth::user());
        }

        /**
        * Update the authenticated user and return the user if successful.
        *
        * @param Request $request
        * @return \Illuminate\Http\JsonResponse
        */
        public function update(Request $request)
        {
            $this->validateUpdate($request);

            $user = Auth::user();
            if ($request->has('user')) {
                $user->update($request->get('user'));
            }

            return new UserResource($user);
        }
    }
    
   |      +---- Middleware
   |            +---- AuthenticateWithJWT.php

    <?php

    /*
    * Custom JWT authentication middleware since the original package does
    * not have a configurable option to change the authorization token name.
    *
    * The token name by default is set to 'bearer'.
    * The default middleware provided does not have any flexibility to
    * change the token name.
    *
    * This project api spec requires us to use the token name 'token'.
    */

    namespace App\Http\Middleware;

    use Closure;
    use Tymon\JWTAuth\Exceptions\JWTException;
    use Tymon\JWTAuth\Exceptions\TokenExpiredException;
    use Tymon\JWTAuth\Exceptions\TokenInvalidException;
    use Tymon\JWTAuth\Http\Middleware\BaseMiddleware;

    class AuthenticateWithJWT extends BaseMiddleware
    {
        /**
        * Handle an incoming request.
        *
        * @param  \Illuminate\Http\Request $request
        * @param  \Closure $next
        * @param bool $optional
        * @return mixed
        */
        public function handle($request, Closure $next, $optional = null)
        {
            $this->auth->setRequest($request);

            try {
                if (! $user = $this->auth->parseToken('token')->authenticate()) {
                    abort(401, 'JWT error: User not found');
                }
            } catch (TokenExpiredException $e) {
                abort(401, 'JWT error: Token has expired');
            } catch (TokenInvalidException $e) {
                abort(401, 'JWT error: Token is invalid');
            } catch (JWTException $e) {
                if ($optional === null) {
                    abort(401);
                }
            }

            return $next($request);
        }
    }
    
   |      +---- Resources
   |            +---- ArticleResource.php

    <?php

    namespace App\Http\Resources;

    use Illuminate\Support\Str;
    use Illuminate\Http\Resources\Json\Resource;

    class ArticleResource extends Resource
    {
        /**
        * The "data" wrapper that should be applied.
        *
        * @var string
        */
        public static $wrap = 'article';

        /**
        * Transform the resource into an array.
        *
        * @param  \Illuminate\Http\Request  $request
        * @return array
        */
        public function toArray($request)
        {
            return [
                'slug'              => $this->slug,
                'title'             => $this->title,
                'description'       => $this->description,
                'body'              => $this->body,
                'tagList'           => $this->tags->sortBy('name')->pluck('name'),
                'createdAt'         => $this->created_at->toAtomString(),
                'updatedAt'         => $this->updated_at->toAtomString(),
                'favorited'         => $this->favorited,
                'favoritesCount'    => $this->favoritesCount,
                'author' => [
                    'username'  => $this->author->username,
                    'bio'       => $this->author->bio,
                    'image'     => $this->author->image,
                    'following' => $this->author->following,
                ]
            ];
        }

        /**
        * Create new resource collection.
        *
        * @param  mixed  $resource
        * @return \Illuminate\Http\Resources\Json\AnonymousResourceCollection
        */
        public static function collection($resource)
        {
            $collection = parent::collection($resource)->collection;
            $wrap = Str::plural(self::$wrap);
            return [
                $wrap           => $collection,
                $wrap . 'Count' => $collection->count()
            ];
        }
    }
    
   |            +---- CommentResource.php

    <?php

    namespace App\Http\Resources;

    use Illuminate\Support\Str;
    use Illuminate\Http\Resources\Json\Resource;

    class CommentResource extends Resource
    {
        /**
        * The "data" wrapper that should be applied.
        *
        * @var string
        */
        public static $wrap = 'comment';

        /**
        * Transform the resource into an array.
        *
        * @param  \Illuminate\Http\Request  $request
        * @return array
        */
        public function toArray($request)
        {
            return [
                'id'        => $this->id,
                'body'      => $this->body,
                'createdAt' => $this->created_at->toAtomString(),
                'updatedAt' => $this->updated_at->toAtomString(),
                'author' => [
                    'username'  => $this->author->username,
                    'bio'       => $this->author->bio,
                    'image'     => $this->author->image,
                    'following' => $this->author->following,
                ]
            ];
        }

        /**
        * Create new resource collection.
        *
        * @param  mixed  $resource
        * @return \Illuminate\Http\Resources\Json\AnonymousResourceCollection
        */
        public static function collection($resource)
        {
            $collection = parent::collection($resource)->collection;
            if ($collection->count() > 1) {
                return [Str::plural(self::$wrap) => $collection];
            }
            // This is according to API specs, but Postman collection gives an error:
            return [self::$wrap => $collection->first()];
        }
    }
    
   |            +---- ProfileResource.php

    <?php

    namespace App\Http\Resources;

    use Illuminate\Http\Resources\Json\Resource;

    class ProfileResource extends Resource
    {
        /**
        * The "data" wrapper that should be applied.
        *
        * @var string
        */
        public static $wrap = 'profile';

        /**
        * Transform the resource into an array.
        *
        * @param  \Illuminate\Http\Request  $request
        * @return array
        */
        public function toArray($request)
        {
            return [
                'username'  => $this->username,
                'bio'       => $this->bio,
                'image'     => $this->image,
                'following' => $this->following,
            ];
        }
    }
    
   |            +---- UserResource.php

    <?php

    namespace App\Http\Resources;

    use Illuminate\Http\Resources\Json\Resource;

    class UserResource extends Resource
    {
        /**
        * The "data" wrapper that should be applied.
        *
        * @var string
        */
        public static $wrap = 'user';

        /**
        * Transform the resource into an array.
        *
        * @param  \Illuminate\Http\Request  $request
        * @return array
        */
        public function toArray($request)
        {
            return [
                'email'     => $this->email,
                'token'     => $this->token,
                'username'  => $this->username,
                'bio'       => $this->bio,
                'image'     => $this->image,
            ];
        }
    }
    
   |      +---- Validators
   |            +---- ValidatesArticleRequests.php

    <?php

    namespace App\Http\Validators;

    use Illuminate\Http\Request;

    trait ValidatesArticleRequests
    {
        /**
        * Validate new article request input
        *
        * @param  Request $request
        * @throws \Illuminate\Auth\Access\ValidationException
        */
        protected function validateNew(Request $request)
        {
            $this->validate($request, [
                'article.title'         => 'required|string|max:255',
                'article.description'   => 'required|string|max:255',
                'article.body'          => 'required|string',
                'article.tagList'       => 'sometimes|array',
            ]);
        }

        /**
        * Validate update article request input
        *
        * @param  Request $request
        * @throws \Illuminate\Auth\Access\ValidationException
        */
        protected function validateUpdate(Request $request)
        {
            $this->validate($request, [
                'article.title'         => 'sometimes|string|max:255',
                'article.description'   => 'sometimes|string|max:255',
                'article.body'          => 'sometimes|string',
            ]);
        }
    }
    
   |            +---- ValidatesAuthenticationRequests.php

    <?php

    namespace App\Http\Validators;

    use Illuminate\Http\Request;

    trait ValidatesAuthenticationRequests
    {
        /**
        * Validate login request input
        *
        * @param  Request $request
        * @throws \Illuminate\Auth\Access\ValidationException
        */
        protected function validateLogin(Request $request)
        {
            $this->validate($request, [
                'user.email'    => 'required|email|max:255',
                'user.password' => 'required',
            ]);
        }

        /**
        * Validate register request input
        *
        * @param  Request $request
        * @throws \Illuminate\Auth\Access\ValidationException
        */
        protected function validateRegister(Request $request)
        {
            $this->validate($request, [
                'user.username' => 'required|max:50|alpha_num|unique:users,username',
                'user.email'    => 'required|email|max:255|unique:users,email',
                'user.password' => 'required|min:8',
            ]);
        }
    }
    
   |            +---- ValidatesUserRequests.php

    <?php

    namespace App\Http\Validators;

    use Illuminate\Http\Request;

    trait ValidatesUserRequests
    {
        /**
        * Validate update user request input
        *
        * @param  Request $request
        * @throws \Illuminate\Auth\Access\ValidationException
        */
        protected function validateUpdate(Request $request)
        {
            if ($request->user()->email === $request->input('user.email')) {
                $email_rule = 'email';
            } else {
                $email_rule = 'sometimes|email|max:255|unique:users,email';
            }

            $this->validate($request, [
                'user.username' => 'sometimes|max:50|alpha_num|unique:username',
                'user.email'    => $email_rule,
                'user.password' => 'sometimes|min:8',
                'user.bio'      => 'sometimes|nullable|max:255',
                'user.image'    => 'sometimes|nullable|url',
            ]);
        }
    }
    
   +---- Jobs
   |      +---- ExampleJob.php

    <?php

    namespace App\Jobs;

    class ExampleJob extends Job
    {
        /**
        * Create a new job instance.
        *
        * @return void
        */
        public function __construct()
        {
            //
        }

        /**
        * Execute the job.
        *
        * @return void
        */
        public function handle()
        {
            //
        }
    }
    
   |      +---- Job.php

    <?php

    namespace App\Jobs;

    use Illuminate\Bus\Queueable;
    use Illuminate\Queue\SerializesModels;
    use Illuminate\Queue\InteractsWithQueue;
    use Illuminate\Contracts\Queue\ShouldQueue;

    abstract class Job implements ShouldQueue
    {
        /*
        |--------------------------------------------------------------------------
        | Queueable Jobs
        |--------------------------------------------------------------------------
        |
        | This job base class provides a central location to place any logic that
        | is shared across all of your jobs. The trait included with the class
        | provides access to the "queueOn" and "delay" queue helper methods.
        |
        */

        use InteractsWithQueue, Queueable, SerializesModels;
    }
    
   +---- Listeners
   |      +---- ExampleListener.php

    <?php

    namespace App\Listeners;

    use App\Events\ExampleEvent;
    use Illuminate\Queue\InteractsWithQueue;
    use Illuminate\Contracts\Queue\ShouldQueue;

    class ExampleListener
    {
        /**
        * Create the event listener.
        *
        * @return void
        */
        public function __construct()
        {
            //
        }

        /**
        * Handle the event.
        *
        * @param  ExampleEvent  $event
        * @return void
        */
        public function handle(ExampleEvent $event)
        {
            //
        }
    }
    
   +---- Models
   |      +---- Article.php

    <?php

    namespace App\Models;

    use App\RealWorld\Slug\HasSlug;
    use App\RealWorld\Filters\Filterable;
    use App\RealWorld\Favorite\Favoritable;
    use Jenssegers\Mongodb\Eloquent\Model;

    class Article extends Model
    {
        use Favoritable, Filterable, HasSlug;

        /**
        * The attributes that are mass assignable.
        *
        * @var array
        */
        protected $fillable = [
            'slug',
            'title',
            'description',
            'body',
        ];

        /**
        * The relations to eager load on every query.
        *
        * @var array
        */
        protected $with = [
            'author'
        ];

        /**
        * Get the author of the article.
        *
        * @return \Jenssegers\Mongodb\Relations\BelongsTo
        */
        public function author()
        {
            return $this->belongsTo(User::class, 'author_id');
        }

        /**
        * Get all the comments for the article.
        *
        * @return \Illuminate\Database\Eloquent\Collection
        */
        public function comments()
        {
            return $this->embedsMany(Comment::class)->latest();
        }

        /**
        * Get all the tags that belong to the article.
        *
        * @return \Illuminate\Database\Eloquent\Collection
        */
        public function tags()
        {
            return $this->embedsMany(Tag::class);
        }

        /**
        * Get the key name for route model binding.
        *
        * @return string
        */
        public function getRouteKeyName()
        {
            return 'slug';
        }

        /**
        * Get the attribute name to slugify.
        *
        * @return string
        */
        public function getSlugSourceColumn()
        {
            return 'title';
        }

        /**
        * Get list of values which are not allowed for this resource
        *
        * @return array
        */
        public function getBannedSlugValues()
        {
            return ['feed'];
        }
    }
    
   |      +---- Comment.php

    <?php

    namespace App\Models;

    use Jenssegers\Mongodb\Eloquent\Model;

    class Comment extends Model
    {
        /**
        * The attributes that are mass assignable.
        *
        * @var array
        */
        protected $fillable = [
            'body',
            'author_id'
        ];

        /**
        * The relations to eager load on every query.
        *
        * @var array
        */
        protected $with = [
            'author'
        ];

        /**
        * Get the author of the comment.
        *
        * @return \Jenssegers\Mongodb\Relations\BelongsTo
        */
        public function author()
        {
            return $this->belongsTo(User::class, 'author_id');
        }
    }
    
   |      +---- Tag.php

    <?php

    namespace App\Models;

    use Jenssegers\Mongodb\Eloquent\Model;

    class Tag extends Model
    {
        /**
        * Indicates if the model should be timestamped.
        *
        * @var bool
        */
        public $timestamps = false;

        /**
        * The attributes that are mass assignable.
        *
        * @var array
        */
        protected $fillable = [
            'name'
        ];
    }
    
   |      +---- User.php

    <?php

    namespace App\Models;

    use Tymon\JWTAuth\Facades\JWTAuth;
    use Tymon\JWTAuth\Contracts\JWTSubject;
    use Illuminate\Auth\Authenticatable;
    use Laravel\Lumen\Auth\Authorizable;
    use Jenssegers\Mongodb\Eloquent\Model;
    use App\RealWorld\Follow\Followable;
    use App\RealWorld\Favorite\HasFavorite;
    use Illuminate\Contracts\Auth\Authenticatable as AuthenticatableContract;
    use Illuminate\Contracts\Auth\Access\Authorizable as AuthorizableContract;

    class User extends Model implements AuthenticatableContract, AuthorizableContract, JWTSubject
    {
        use Authenticatable, Authorizable, Followable, HasFavorite;

        /**
        * The attributes that are mass assignable.
        *
        * @var array
        */
        protected $fillable = [
            'username',
            'email',
            'password',
            'bio',
            'image'
        ];

        /**
        * The attributes that should be hidden for arrays.
        *
        * @var array
        */
        protected $hidden = [
            'password',
            'remember_token'
        ];

        /**
        * Get all the articles by the user.
        *
        * @return \Jenssegers\Mongodb\Relations\HasMany
        */
        public function articles()
        {
            return $this->hasMany(Article::class, 'author_id')->latest();
        }

        /**
        * Get all the comments by the user.
        *
        * @return \Jenssegers\Mongodb\Relations\HasMany
        */
        public function comments()
        {
            return $this->hasMany(Comment::class, 'author_id')->latest();
        }

        /**
        * Get all the articles of the following users.
        *
        * @return \Illuminate\Database\Eloquent\Relations\HasMany
        */
        public function feed()
        {
            $followingIds = $this->following->pluck('id')->toArray();
            return Article::whereIn('author_id', $followingIds)->get();
        }

        /**
        * Generate a JWT token for the user.
        *
        * @return string
        */
        public function getTokenAttribute()
        {
            return JWTAuth::fromUser($this);
        }

        /**
        * @return int
        */
        public function getJWTIdentifier()
        {
            return $this->getKey();
        }

        /**
        * @return array
        */
        public function getJWTCustomClaims()
        {
            return [];
        }
    }
    
   +---- Providers
   |      +---- AppServiceProvider.php

    <?php

    namespace App\Providers;

    use Illuminate\Support\ServiceProvider;

    class AppServiceProvider extends ServiceProvider
    {
        /**
        * Register any application services.
        *
        * @return void
        */
        public function register()
        {
            //
        }
    }
    
   |      +---- AuthServiceProvider.php

    <?php

    namespace App\Providers;

    use App\Models\User;
    use Illuminate\Support\Facades\Auth;
    use Illuminate\Support\Facades\Gate;
    use Illuminate\Support\ServiceProvider;

    class AuthServiceProvider extends ServiceProvider
    {
        /**
        * Register any application services.
        *
        * @return void
        */
        public function register()
        {
        }

        /**
        * Boot the authentication services for the application.
        *
        * @return void
        */
        public function boot()
        {
            Gate::define('update-article', function ($user, $article) {
                return $user->id === $article->author_id;
            });

            Gate::define('delete-article', function ($user, $article) {
                return $user->id === $article->author_id;
            });

            Gate::define('delete-comment', function ($user, $comment) {
                return $user->id === $comment->author_id;
            });

            Gate::define('favorite-article', function ($user, $article) {
                return $user->id != $article->author_id;
            });
        }
    }
    
   |      +---- EventServiceProvider.php

    <?php

    namespace App\Providers;

    use Laravel\Lumen\Providers\EventServiceProvider as ServiceProvider;

    class EventServiceProvider extends ServiceProvider
    {
        /**
        * The event listener mappings for the application.
        *
        * @var array
        */
        protected $listen = [
            'App\Events\SomeEvent' => [
                'App\Listeners\EventListener',
            ],
        ];
    }
    
   |      +---- JwtServiceProvider.php

    <?php

    /*
    * Custom JWT authentication service provider since the original package does
    * not have a configurable option to change the authorization token name.
    *
    * The token name by default is set to 'bearer'.
    * This project api spec requires us to use the token name 'token'.
    */

    namespace App\Providers;

    use Tymon\JWTAuth\Http\Parser\AuthHeaders;
    use Tymon\JWTAuth\Http\Parser\InputSource;
    use Tymon\JWTAuth\Http\Parser\QueryString;
    use Tymon\JWTAuth\Http\Parser\LumenRouteParams;
    use Tymon\JWTAuth\Providers\AbstractServiceProvider;

    class JwtServiceProvider extends AbstractServiceProvider
    {
        /**
        * {@inheritdoc}
        */
        public function boot()
        {
            $this->app->configure('jwt');

            $path = realpath(__DIR__.'/../../vendor/tymon/jwt-auth/config/config.php');
            $this->mergeConfigFrom($path, 'jwt');

            $this->app->routeMiddleware($this->middlewareAliases);

            $this->extendAuthGuard();

            $this->app['tymon.jwt.parser']->setChain([
                with(new AuthHeaders)->setHeaderPrefix('token'),
                new QueryString,
                new InputSource,
                new LumenRouteParams,
            ]);
        }
    }
    
   +---- RealWorld
   |      +---- Favorite
   |            +---- Favoritable.php

    <?php

    namespace App\RealWorld\Favorite;

    use Auth;
    use App\Models\User;

    trait Favoritable
    {
        /**
        * Check if the authenticated user has favorited the article.
        *
        * @return bool
        */
        public function getFavoritedAttribute()
        {
            if (! Auth::check() || ! $this->favorited_by_ids) {
                return false;
            }
            return in_array(Auth::user()->id, $this->favorited_by_ids, true);
        }

        /**
        * Get the favorites count of the article.
        *
        * @return integer
        */
        public function getFavoritesCountAttribute()
        {
            if (array_key_exists('favorited_count', $this->getAttributes())) {
                return $this->favorited_count;
            }
            return $this->favoritedBy()->count();
        }

        /**
        * Get the users that favorited the article.
        *
        * @return \Jenssegers\Mongodb\Relations\BelongsToMany
        */
        public function favoritedBy()
        {
            return $this->belongsToMany(User::class, null, 'favorite_article_ids', 'favorited_by_ids');
        }

        /**
        * Check if the article is favorited by the given user.
        *
        * @param User $user
        * @return bool
        */
        public function isFavoritedBy(User $user)
        {
            return !! $this->favoritedBy()->where('favorited_by_ids', $user->id)->count();
        }
    }
    
   |            +---- HasFavorite.php

    <?php

    namespace App\RealWorld\Favorite;

    use Auth;
    use App\Models\Article;

    trait HasFavorite
    {
        /**
        * Favorite the given article.
        *
        * @param Article $article
        * @return mixed
        */
        public function favorite(Article $article)
        {
            if (! $this->hasFavorited($article)) {
                $article->favoritedBy()->attach(Auth::user());
            }
        }

        /**
        * Unfavorite the given article.
        *
        * @param Article $article
        * @return mixed
        */
        public function unFavorite(Article $article)
        {
            $article->favoritedBy()->detach(Auth::user());
        }

        /**
        * Get the articles favorited by the user.
        *
        * @return \Jenssegers\Mongodb\Relations\BelongsToMany
        */
        public function favorites()
        {
            return $this->belongsToMany(Article::class, null, 'favorited_by_ids', 'favorite_article_ids');
        }

        /**
        * Check if the user has favorited the given article.
        *
        * @param Article $article
        * @return bool
        */
        public function hasFavorited(Article $article)
        {
            return !! $this->favorites()->where('favorite_article_ids', $article->id)->count();
        }
    }
    
   |      +---- Filters
   |            +---- ArticleFilter.php

    <?php

    namespace App\RealWorld\Filters;

    use App\Models\Tag;
    use App\Models\User;
    use App\Models\Article;

    class ArticleFilter extends Filter
    {
        /**
        * Filter by author username.
        * Get all the articles by the user with given username.
        *
        * @param $username
        * @return \Illuminate\Database\Eloquent\Collection
        */
        protected function author($username)
        {
            $user = User::whereUsername($username)->first();
            $user_id = $user ? $user->id : null;

            return $this->collection->where('author_id', $user_id);
        }

        /**
        * Filter by favorited username.
        * Get all the articles favorited by the user with given username.
        *
        * @param $username
        * @return \Illuminate\Database\Eloquent\Collection
        */
        protected function favorited($username)
        {
            $user = User::whereUsername($username)->first();
            return $user->favorites->intersect($this->collection);
        }

        /**
        * Filter by tag name.
        * Get all the articles tagged by the given tag name.
        *
        * @param $name
        * @return \Illuminate\Database\Eloquent\Collection
        */
        protected function tag($name)
        {
            $articles = Article::whereRaw(['tags.name' => $name])->get();
            return $articles->intersect($this->collection);
        }
    }
    
   |            +---- Filter.php

    <?php

    namespace App\RealWorld\Filters;

    use ReflectionClass;
    use Illuminate\Http\Request;
    use Jenssegers\Mongodb\Eloquent\Builder;
    use Illuminate\Database\Eloquent\Collection;

    abstract class Filter
    {
        /**
        * @var \Illuminate\Http\Request
        */
        protected $request;

        /**
        * @var \Illuminate\Database\Eloquent\Collection
        */
        protected $collection;

        /**
        * Filter constructor.
        *
        * @param \Illuminate\Http\Request $request
        */
        public function __construct(Request $request)
        {
            $this->request = $request;
        }

        /**
        * Get all the available filter methods.
        *
        * @return array
        */
        protected function getFilterMethods()
        {
            $class  = new ReflectionClass(static::class);

            $methods = array_map(function ($method) use ($class) {
                if ($method->class === $class->getName()) {
                    return $method->name;
                }
                return null;
            }, $class->getMethods());

            return array_filter($methods);
        }

        /**
        * Get all the filters that can be applied.
        *
        * @return array
        */
        protected function getFilters()
        {
            return array_filter($this->request->only($this->getFilterMethods()));
        }

        /**
        * Apply all the requested filters if available.
        *
        * @param \Jenssegers\Mongodb\Eloquent\Builder $builder
        * @return \Illuminate\Database\Eloquent\Collection
        */
        public function apply(Builder $builder)
        {
            $this->collection = $builder->latest()->get();

            foreach ($this->getFilters() as $name => $value) {
                if (method_exists($this, $name)) {
                    if ($value) {
                        $this->collection = $this->$name($value);
                    } else {
                        $this->collection = $this->$name();
                    }
                }
            }

            return $this->collection->values();
        }
    }
    
   |            +---- Filterable.php

    <?php

    namespace App\RealWorld\Filters;

    use Jenssegers\Mongodb\Eloquent\Builder;

    trait Filterable
    {
        /**
        * Scope a query to apply given filter.
        *
        * @param \Jenssegers\Mongodb\Eloquent\Builder $builder
        * @param Filter $filter
        * @return \Jenssegers\Mongodb\Eloquent\Builder
        */
        public function scopeFilter(Builder $builder, Filter $filter)
        {
            return $filter->apply($builder);
        }
    }
    
   |      +---- Follow
   |            +---- Followable.php

    <?php

    namespace App\RealWorld\Follow;

    use Auth;
    use App\Models\User;

    trait Followable
    {
        /**
        * Check if the authenticated user is following this user.
        *
        * @return bool
        */
        public function getFollowingAttribute()
        {
            if (! Auth::check() || ! $this->follower_ids) {
                return false;
            }
            return in_array(Auth::user()->id, $this->follower_ids, true);
        }

        /**
        * Follow the given user.
        *
        * @param User $user
        * @return mixed
        */
        public function follow(User $user)
        {
            if ($this->id != $user->id) {
                $user->followers()->attach(Auth::user());
            }
        }

        /**
        * Unfollow the given user.
        *
        * @param User $user
        * @return mixed
        */
        public function unFollow(User $user)
        {
            $user->followers()->detach(Auth::user());
        }

        /**
        * Get all the users that this user is following.
        *
        * @return \Jenssegers\Mongodb\Relations\BelongsToMany
        */
        public function follows()
        {
            return $this->belongsToMany(User::class, null, 'follower_ids', 'following_ids');
        }

        /**
        * Get all the users that are following this user.
        *
        * @return \Jenssegers\Mongodb\Relations\BelongsToMany
        */
        public function followers()
        {
            return $this->belongsToMany(User::class, null, 'following_ids', 'follower_ids');
        }

        /**
        * Check if a given user is following this user.
        *
        * @param User $user
        * @return bool
        */
        public function isFollowing(User $user)
        {
            return !! $this->follows()->where('following_ids', $user->id)->count();
        }

        /**
        * Check if a given user is being followed by this user.
        *
        * @param User $user
        * @return bool
        */
        public function isFollowedBy(User $user)
        {
            return !! $this->followers()->where('follower_ids', $user->id)->count();
        }
    }
    
   |      +---- Slug
   |            +---- HasSlug.php

    <?php

    namespace App\RealWorld\Slug;

    trait HasSlug
    {
        /**
        * Adding or updating slug when attribute to "slugify" is set.
        *
        * @param string $key
        * @param mixed $value
        * @return $this
        */
        public function setAttribute($key, $value)
        {
            if ($key == $this->getSlugSourceColumn()) {
                $slug = (new Slug($value))
                    ->uniqueFor($this)
                    ->without($this->getBannedSlugValues())
                    ->generate();

                $this->attributes[$this->getSlugSourceColumn()] = $value;
                $this->attributes[$this->getSlugColumn()] = $slug;
            }

            return parent::setAttribute($key, $value);
        }

        /**
        * Get the attribute name to slugify.
        *
        * @return string
        */
        abstract public function getSlugSourceColumn();

        /**
        * Get the name of the slug column
        *
        * @return string
        */
        public function getSlugColumn()
        {
            return 'slug';
        }

        /**
        * Get list of values wich are not allowed
        *
        * @return array
        */
        public function getBannedSlugValues()
        {
            return [];
        }
    }
    
   |            +---- Slug.php

    <?php

    namespace App\RealWorld\Slug;

    use Illuminate\Database\Eloquent\Model;

    class Slug
    {
        /**
        * Eloquent model used for example for uniqueness
        *
        * @var Model
        */
        protected $model;

        /**
        * Banned values for slug generation
        *
        * @var array
        */
        protected $banned = [];

        /**
        * Initial value to slugify
        *
        * @var string
        */
        private $initialValue;

        /**
        * Separator use to generate slugs
        *
        * @var string
        */
        const SEPARATOR = '-';

        /**
        * Slug constructor.
        *
        * @param string $value
        */
        public function __construct($value)
        {
            $this->initialValue = $value;
        }

        /**
        * Generate a unique slug
        *
        * @return string
        */
        public function generate()
        {
            $slug = str_slug($this->initialValue, static::SEPARATOR);

            $notAllowed = $this->getSimilarSlugs($slug)->merge($this->banned);

            if ($notAllowed->isEmpty() || !$notAllowed->contains($slug)) {
                return $slug;
            }

            $suffix = $this->generateSuffix($slug, $notAllowed);

            return $slug . static::SEPARATOR . $suffix;
        }

        /**
        * Generate suffix for unique slug
        *
        * @param string $slug
        * @param \Illuminate\Support\Collection $notAllowed
        * @return string
        */
        public function generateSuffix($slug, $notAllowed)
        {
            /** @var \Illuminate\Support\Collection $notAllowed */
            $notAllowed->transform(function ($item) use ($slug) {

                if ($slug == $item) {
                    return 0;
                }

                return (int)str_replace($slug . static::SEPARATOR, '', $item);
            });

            return $notAllowed->max() + 1;
        }

        /**
        * Set eloquent model to check uniqueness on.
        *
        * @param \Illuminate\Database\Eloquent\Model $model
        * @return $this
        */
        public function uniqueFor(Model $model)
        {
            $this->model = $model;

            return $this;
        }

        /**
        * Set array of values which are not allowed.
        *
        * @param $values
        * @return $this
        */
        public function without($values)
        {
            $this->banned = $values;

            return $this;
        }

        /**
        * Get collection of similar slugs based on database
        *
        * @param $slug
        * @return \Illuminate\Support\Collection
        */
        private function getSimilarSlugs($slug)
        {
            if (!$this->model instanceof Model || !method_exists($this->model, 'getSlugColumn')) {
                return collect([]);
            }

            $slugColumn = $this->model->getSlugColumn();

            return $this->model->newQuery()
                ->where($slugColumn, $slug)
                ->orWhere($slugColumn, 'LIKE', $slug . static::SEPARATOR . '%')
                ->get()
                ->pluck($slugColumn);
        }
    }
    
   bootstrap
   +---- app.php

    <?php

    require_once __DIR__ . '/../vendor/autoload.php';

    (new Laravel\Lumen\Bootstrap\LoadEnvironmentVariables(
        dirname(__DIR__)
    ))->bootstrap();

    /*
    |--------------------------------------------------------------------------
    | Create The Application
    |--------------------------------------------------------------------------
    |
    | Here we will load the environment and create the application instance
    | that serves as the central piece of this framework. We'll use this
    | application as an "IoC" container and router for this framework.
    |
    */

    $app = new Laravel\Lumen\Application(
        realpath(__DIR__ . '/../')
    );

    $app->withFacades();

    /*
    |--------------------------------------------------------------------------
    | Register Container Bindings
    |--------------------------------------------------------------------------
    |
    | Now we will register a few bindings in the service container. We will
    | register the exception handler and the console kernel. You may add
    | your own bindings here if you like or you can make another file.
    |
    */

    $app->singleton(
        Illuminate\Contracts\Debug\ExceptionHandler::class,
        App\Exceptions\Handler::class
    );

    $app->singleton(
        Illuminate\Contracts\Console\Kernel::class,
        App\Console\Kernel::class
    );

    /*
    |--------------------------------------------------------------------------
    | Register Middleware
    |--------------------------------------------------------------------------
    |
    | Next, we will register the middleware with the application. These can
    | be global middleware that run before and after each request into a
    | route or middleware that'll be assigned to some specific routes.
    |
    */

    $app->middleware([
        \Barryvdh\Cors\HandleCors::class,
    ]);

    $app->routeMiddleware([
        'auth' => App\Http\Middleware\AuthenticateWithJWT::class,
    ]);

    /*
    |--------------------------------------------------------------------------
    | Register Service Providers
    |--------------------------------------------------------------------------
    |
    | Here we will register all of the application's service providers which
    | are used to bind services into the container. Service providers are
    | totally optional, so you are not required to uncomment this line.
    |
    */

    $app->register(App\Providers\AppServiceProvider::class);
    $app->register(App\Providers\AuthServiceProvider::class);
    // $app->register(App\Providers\EventServiceProvider::class);
    $app->register(App\Providers\JwtServiceProvider::class);
    $app->register(Jenssegers\Mongodb\MongodbServiceProvider::class);
    $app->register(Barryvdh\Cors\ServiceProvider::class);

    $app->withEloquent();

    /*
    |--------------------------------------------------------------------------
    | Load The Application Routes
    |--------------------------------------------------------------------------
    |
    | Next we will include the routes file so that they can all be added to
    | the application. This will provide all of the URLs the application
    | can respond to, as well as the controllers that may handle them.
    |
    */

    $app->router->group([
        'namespace' => 'App\Http\Controllers',
    ], function ($router) {
        require __DIR__ . '/../routes/web.php';
    });

    return $app;
    
   composer.json

    {
        "name": "elcobvg/lumen-realworld-example-app",
        "description": "Exemplary real world backend API built with Lumen + MongoDB",
        "keywords": ["laravel", "lumen", "mongodb", "jwt", "examples"],
        "authors": [
            {
                "name": "Elco Brouwer von Gonzenbach",
                "email": "elco.brouwer@gmail.com"
            }
        ],
        "license": "MIT",
        "type": "project",
        "require": {
            "php": ">=7.1",
            "barryvdh/laravel-cors": "^0.11",
            "jenssegers/mongodb": "^3.3",
            "laravel/lumen-framework": "5.8.*",
            "tymon/jwt-auth": "dev-develop",
            "vlucas/phpdotenv": "~3.3"
        },
        "require-dev": {
            "fzaninotto/faker": "~1.4",
            "mockery/mockery": "~1.0",
            "phpunit/phpunit": "~7.0",
            "squizlabs/php_codesniffer": "^3.1"
        },
        "autoload": {
            "psr-4": {
                "App\\": "app/"
            }
        },
        "autoload-dev": {
            "classmap": [
                "tests/",
                "database/"
            ]
        },
        "scripts": {
            "post-root-package-install": [
                "php -r \"copy('.env.example', '.env');\""
            ]
        },
        "config": {
            "sort-packages": true,
            "optimize-autoloader": true
        }
    }
    
   config
   +---- app.php

    <?php

    return [

        /*
        |--------------------------------------------------------------------------
        | Application Name
        |--------------------------------------------------------------------------
        |
        | This value is the name of your application. This value is used when the
        | framework needs to place the application's name in a notification or
        | any other location as required by the application or its packages.
        |
        */

        'name' => env('APP_NAME', 'Lumen Real World Demo App'),

        /*
        |--------------------------------------------------------------------------
        | Application Environment
        |--------------------------------------------------------------------------
        |
        | This value determines the "environment" your application is currently
        | running in. This may determine how you prefer to configure various
        | services your application utilizes. Set this in your ".env" file.
        |
        */

        'env' => env('APP_ENV', 'production'),

        /*
        |--------------------------------------------------------------------------
        | Application Debug Mode
        |--------------------------------------------------------------------------
        |
        | When your application is in debug mode, detailed error messages with
        | stack traces will be shown on every error that occurs within your
        | application. If disabled, a simple generic error page is shown.
        |
        */

        'debug' => env('APP_DEBUG', false),

        /*
        |--------------------------------------------------------------------------
        | Application URL
        |--------------------------------------------------------------------------
        |
        | This URL is used by the console to properly generate URLs when using
        | the Artisan command line tool. You should set this to the root of
        | your application so that it is used when running Artisan tasks.
        |
        */

        'url' => env('APP_URL', 'http://localhost'),

        /*
        |--------------------------------------------------------------------------
        | Application Timezone
        |--------------------------------------------------------------------------
        |
        | Here you may specify the default timezone for your application, which
        | will be used by the PHP date and date-time functions. We have gone
        | ahead and set this to a sensible default for you out of the box.
        |
        */

        'timezone' => 'UTC',

        /*
        |--------------------------------------------------------------------------
        | Application Locale Configuration
        |--------------------------------------------------------------------------
        |
        | The application locale determines the default locale that will be used
        | by the translation service provider. You are free to set this value
        | to any of the locales which will be supported by the application.
        |
        */

        'locale' => 'en',

        /*
        |--------------------------------------------------------------------------
        | Application Fallback Locale
        |--------------------------------------------------------------------------
        |
        | The fallback locale determines the locale to use when the current one
        | is not available. You may change the value to correspond to any of
        | the language folders that are provided through your application.
        |
        */

        'fallback_locale' => 'en',

        /*
        |--------------------------------------------------------------------------
        | Encryption Key
        |--------------------------------------------------------------------------
        |
        | This key is used by the Illuminate encrypter service and should be set
        | to a random, 32 character string, otherwise these encrypted strings
        | will not be safe. Please do this before deploying an application!
        |
        */

        'key' => env('APP_KEY'),

        'cipher' => 'AES-256-CBC',

        /*
        |--------------------------------------------------------------------------
        | Logging Configuration
        |--------------------------------------------------------------------------
        |
        | Here you may configure the log settings for your application. Out of
        | the box, Laravel uses the Monolog PHP logging library. This gives
        | you a variety of powerful log handlers / formatters to utilize.
        |
        | Available Settings: "single", "daily", "syslog", "errorlog"
        |
        */

        'log' => env('APP_LOG', 'single'),

        'log_level' => env('APP_LOG_LEVEL', 'debug'),
    ];
    
   +---- auth.php

    <?php

    return [

        /*
        |--------------------------------------------------------------------------
        | Authentication Defaults
        |--------------------------------------------------------------------------
        |
        | This option controls the default authentication "guard" and password
        | reset options for your application. You may change these defaults
        | as required, but they're a perfect start for most applications.
        |
        */

        'defaults' => [
            'guard' => env('AUTH_GUARD', 'api'),
            'passwords' => 'users',
        ],

        /*
        |--------------------------------------------------------------------------
        | Authentication Guards
        |--------------------------------------------------------------------------
        |
        | Next, you may define every authentication guard for your application.
        | Of course, a great default configuration has been defined for you
        | here which uses session storage and the Eloquent user provider.
        |
        | All authentication drivers have a user provider. This defines how the
        | users are actually retrieved out of your database or other storage
        | mechanisms used by this application to persist your user's data.
        |
        | Supported: "session", "token"
        |
        */

        'guards' => [
            'api' => [
                'driver' => 'jwt',
                'provider' => 'users',
            ],
        ],

        /*
        |--------------------------------------------------------------------------
        | User Providers
        |--------------------------------------------------------------------------
        |
        | All authentication drivers have a user provider. This defines how the
        | users are actually retrieved out of your database or other storage
        | mechanisms used by this application to persist your user's data.
        |
        | If you have multiple user tables or models you may configure multiple
        | sources which represent each model / table. These sources may then
        | be assigned to any extra authentication guards you have defined.
        |
        | Supported: "database", "eloquent"
        |
        */

        'providers' => [
            'users' => [
                'driver' => 'eloquent',
                'model' => \App\Models\User::class,
            ],
        ],
    ];
    
   +---- cors.php

    <?php

    return [
        /*
        |--------------------------------------------------------------------------
        | Laravel CORS
        |--------------------------------------------------------------------------
        |
        | allowedOrigins, allowedHeaders and allowedMethods can be set to array('*')
        | to accept any value.
        |
        */
        'supportsCredentials' => true,
        'allowedOrigins' => ['*'],
        'allowedHeaders' => ['*'],
        'allowedMethods' => ['GET', 'POST', 'PUT', 'DELETE', 'OPTIONS'],
        'exposedHeaders' => [],
        'maxAge' => 0,
        'hosts' => [],
    ];
    
   +---- database.php

    <?php

    return [

        /*
        |--------------------------------------------------------------------------
        | Default Database Connection Name
        |--------------------------------------------------------------------------
        |
        | Here you may specify which of the database connections below you wish
        | to use as your default connection for all database work. Of course
        | you may use many connections at once using the Database library.
        |
        */

        'default' => env('DB_CONNECTION', 'mongodb'),

        /*
        |--------------------------------------------------------------------------
        | Database Connections
        |--------------------------------------------------------------------------
        |
        | Here are each of the database connections setup for your application.
        | Of course, examples of configuring each database platform that is
        | supported by Laravel is shown below to make development simple.
        |
        |
        | All database work in Laravel is done through the PHP PDO facilities
        | so make sure you have the driver for your particular database of
        | choice installed on your machine before you begin development.
        |
        */

        'connections' => [

            'mongodb' => [
                'driver'   => 'mongodb',
                'host'     => env('DB_HOST', 'localhost'),
                'port'     => env('DB_PORT', 27017),
                'database' => env('DB_DATABASE', 'local'),
                'username' => env('DB_USERNAME'),
                'password' => env('DB_PASSWORD'),
                'options'  => [
                    'database' => env('DB_DATABASE', 'admin') // sets the authentication database required by mongo 3
                ]
            ],

            'sqlite' => [
                'driver' => 'sqlite',
                'database' => env('DB_DATABASE', database_path('database.sqlite')),
                'prefix' => '',
            ],

            'mysql' => [
                'driver' => 'mysql',
                'host' => env('DB_HOST', '127.0.0.1'),
                'port' => env('DB_PORT', '3306'),
                'database' => env('DB_DATABASE', 'forge'),
                'username' => env('DB_USERNAME', 'forge'),
                'password' => env('DB_PASSWORD', ''),
                'unix_socket' => env('DB_SOCKET', ''),
                'charset' => 'utf8mb4',
                'collation' => 'utf8mb4_unicode_ci',
                'prefix' => '',
                'strict' => true,
                'engine' => null,
            ],

            'pgsql' => [
                'driver' => 'pgsql',
                'host' => env('DB_HOST', '127.0.0.1'),
                'port' => env('DB_PORT', '5432'),
                'database' => env('DB_DATABASE', 'forge'),
                'username' => env('DB_USERNAME', 'forge'),
                'password' => env('DB_PASSWORD', ''),
                'charset' => 'utf8',
                'prefix' => '',
                'schema' => 'public',
                'sslmode' => 'prefer',
            ],

            'sqlsrv' => [
                'driver' => 'sqlsrv',
                'host' => env('DB_HOST', 'localhost'),
                'port' => env('DB_PORT', '1433'),
                'database' => env('DB_DATABASE', 'forge'),
                'username' => env('DB_USERNAME', 'forge'),
                'password' => env('DB_PASSWORD', ''),
                'charset' => 'utf8',
                'prefix' => '',
            ],

        ],

        /*
        |--------------------------------------------------------------------------
        | Migration Repository Table
        |--------------------------------------------------------------------------
        |
        | This table keeps track of all the migrations that have already run for
        | your application. Using this information, we can determine which of
        | the migrations on disk haven't actually been run in the database.
        |
        */

        'migrations' => 'migrations',

        /*
        |--------------------------------------------------------------------------
        | Redis Databases
        |--------------------------------------------------------------------------
        |
        | Redis is an open source, fast, and advanced key-value store that also
        | provides a richer set of commands than a typical key-value systems
        | such as APC or Memcached. Laravel makes it easy to dig right in.
        |
        */

        'redis' => [

            'client' => 'predis',

            'default' => [
                'host' => env('REDIS_HOST', '127.0.0.1'),
                'password' => env('REDIS_PASSWORD', null),
                'port' => env('REDIS_PORT', 6379),
                'database' => 0,
            ],

        ],

    ];
    
   database
   +---- factories
   |      +---- ModelFactory.php

    <?php

    /*
    |--------------------------------------------------------------------------
    | Model Factories
    |--------------------------------------------------------------------------
    |
    | Here you may define all of your model factories. Model factories give
    | you a convenient way to create models for testing and seeding your
    | database. Just tell the factory how a default model should look.
    |
    */

    $factory->define(App\Models\User::class, function (Faker\Generator $faker) {

        $number = $faker->numberBetween(1, 99);
        $gender = $faker->randomElement(['men', 'women']);
        gc_collect_cycles();

        return [
            'username'  => str_replace('.', '', $faker->unique()->userName),
            'email'     => $faker->unique()->email,
            'password'  => \Illuminate\Support\Facades\Hash::make('password'),
            'bio'       => $faker->sentence(10),
            'image'     => "https://randomuser.me/api/portraits/{$gender}/{$number}.jpg"
        ];
    });

    $factory->define(App\Models\Article::class, function (Faker\Generator $faker) {
        gc_collect_cycles();

        return [
            'title'         => $faker->sentence,
            'description'   => $faker->paragraph,
            'body'          => implode('<p>', $faker->paragraphs),
        ];
    });

    $factory->define(App\Models\Comment::class, function (Faker\Generator $faker) {
        return [
            'body'  => $faker->paragraph,
        ];
    });

    $factory->define(App\Models\Tag::class, function (Faker\Generator $faker) {
        return [
            'name'  => $faker->randomElement([
                'apples',
                'bananas',
                'cherries',
                'dates',
                'figs',
                'grapes',
                'kiwis',
                'limes',
                'melons',
                'oranges',
                'pears',
                'strawberries',
            ]),
        ];
    });
    
   +---- migrations
   |      +---- 2018_01_03_023000_create_users_collection.php

    <?php

    use Illuminate\Support\Facades\Schema;
    use Illuminate\Database\Schema\Blueprint;
    use Illuminate\Database\Migrations\Migration;

    class CreateUsersCollection extends Migration
    {
        /**
        * Run the migrations.
        *
        * @return void
        */
        public function up()
        {
            Schema::create('users', function (Blueprint $collection) {
                $collection->unique('email');
                $collection->unique('username');
            });
        }

        /**
        * Reverse the migrations.
        *
        * @return void
        */
        public function down()
        {
            Schema::collection('users', function (Blueprint $collection) {
                $collection->drop();
            });
        }
    }
    
   |      +---- 2018_01_03_025149_create_articles_collection.php

    <?php

    use Illuminate\Support\Facades\Schema;
    use Illuminate\Database\Schema\Blueprint;
    use Illuminate\Database\Migrations\Migration;

    class CreateArticlesCollection extends Migration
    {
        /**
        * Run the migrations.
        *
        * @return void
        */
        public function up()
        {
            Schema::create('articles', function (Blueprint $collection) {
                $collection->unique('slug');
                $collection->index('author_id');
            });
        }

        /**
        * Reverse the migrations.
        *
        * @return void
        */
        public function down()
        {
            Schema::collection('articles', function (Blueprint $collection) {
                $collection->drop();
            });
        }
    }
    
   +---- seeds
   |      +---- ArticlesTableSeeder.php

    <?php

    use Illuminate\Database\Seeder;

    class ArticlesTableSeeder extends Seeder
    {
        /**
        * Run the database seeds.
        *
        * @return void
        */
        public function run()
        {
            factory(App\Models\Article::class, 25)->create()->each(function ($article) {

                $faker = Faker\Factory::create();
                gc_collect_cycles();

                $authors = App\Models\User::all();
                $author = $authors[$faker->numberBetween(0, sizeof($authors) - 1)];
                $article->author()->associate($author);

                $num_com = $faker->numberBetween(1, 5);
                for ($i = 0; $i < $num_com; $i++) {
                    $comment = factory(App\Models\Comment::class)->make();
                    $comment->author()->associate($authors[$faker->numberBetween(0, sizeof($authors) - 1)]);
                    $article->comments()->save($comment);
                }

                $num_tags = $faker->numberBetween(0, 4);
                for ($j = 0; $j < $num_tags; $j++) {
                    $tag = factory(App\Models\Tag::class)->make();
                    $article->tags()->save($tag);
                }

                $users = App\Models\User::where('id', '<>', $author->id)->get();
                $num_favs = $faker->numberBetween(0, 10);
                for ($k = 0; $k < $num_favs; $k++) {
                    $article->favoritedBy()->attach($users[$faker->numberBetween(0, sizeof($users) - 1)]);
                }

                $article->save();
                $this->command->info('Article created');
            });
        }
    }
    
   |      +---- DatabaseSeeder.php

    <?php

    use Illuminate\Database\Seeder;

    class DatabaseSeeder extends Seeder
    {
        /**
        * Run the database seeds.
        *
        * @return void
        */
        public function run()
        {
            $this->call('UsersTableSeeder');
            $this->call('ArticlesTableSeeder');
        }
    }
    
   |      +---- UsersTableSeeder.php

    <?php

    use Illuminate\Database\Seeder;

    class UsersTableSeeder extends Seeder
    {
        /**
        * Run the database seeds.
        *
        * @return void
        */
        public function run()
        {
            factory(App\Models\User::class, 10)->create();
            $this->command->info('Users created');
            gc_collect_cycles();

            $users = App\Models\User::all();
            foreach ($users as $user) {
                $faker = Faker\Factory::create();

                $others = App\Models\User::where('id', '<>', $user->id)->get();

                $num = $faker->numberBetween(1, 5);
                for ($i = 0; $i < $num; $i++) {
                    $user->follow($others[$faker->numberBetween(0, sizeof($others) - 1)]);
                }
            }
            $this->command->info('Followers added');
        }
    }
    
   public
   +---- index.php

    <?php

    /*
    |--------------------------------------------------------------------------
    | Create The Application
    |--------------------------------------------------------------------------
    |
    | First we need to get an application instance. This creates an instance
    | of the application / container and bootstraps the application so it
    | is ready to receive HTTP / Console requests from the environment.
    |
    */

    $app = require __DIR__.'/../bootstrap/app.php';

    /*
    |--------------------------------------------------------------------------
    | Run The Application
    |--------------------------------------------------------------------------
    |
    | Once we have the application, we can handle the incoming request
    | through the kernel, and send the associated response back to
    | the client's browser allowing them to enjoy the creative
    | and wonderful application we have prepared for them.
    |
    */

    $app->run();
    
   readme.md

    # ![RealWorld Example App](logo.png)

    > ### Lumen + MongoDB codebase containing real world examples (CRUD, auth, advanced patterns, etc) that adheres to the [RealWorld](https://github.com/gothinkster/realworld) spec and API.


    ### [Demo](https://lumen-realworld.herokuapp.com/)&nbsp;&nbsp;&nbsp;&nbsp;[RealWorld](https://github.com/gothinkster/realworld)


    This codebase was created to demonstrate a fully functional REST API built with **Lumen + MongoDB**, including CRUD operations, authentication, routing, pagination, and more.

    It borrows heavily from the [excellent Laravel implementation](https://github.com/gothinkster/laravel-realworld-example-app) by [SandeeshS](https://github.com/SandeeshS).

    For more information on how this works with other frontends/backends, head over to the [RealWorld](https://github.com/gothinkster/realworld) repo.

    Hope you'll find this example helpful. Pull requests are welcome!

    ----------

    # Getting started

    ## Installation

    Please check the official Lumen installation guide for server requirements before you start. [Official Documentation](https://lumen.laravel.com/docs/5.5/installation)


    Clone the repository

        git clone git@github.com:elcobvg/lumen-realworld-example-app.git

    Switch to the repo folder

        cd lumen-realworld-example-app

    Install all the dependencies using composer

        composer install

    Copy the example env file and make the required configuration changes in the .env file

        cp .env.example .env

    Generate a new application key

    Since Lumen doesn't have the `php artisan key:generate` command, there's a custom route `http://localhost:8000/appKey` to help you generate an application key.

    Generate a new JWT authentication secret key

        php artisan jwt:secret

    Run the database migrations (**Set the database connection in .env before migrating**)

        php artisan migrate

    Start the local development server

        php -S localhost:8000 -t public

    You can now access the server at http://localhost:8000

    **TL;DR command list**

        git clone git@elcobvg/lumen-realworld-example-app.git
        cd lumen-realworld-example-app
        composer install
        cp .env.example .env
        php artisan key:generate
        php artisan jwt:secret

    **Make sure you set the correct database connection information before running the migrations** [Environment variables](#environment-variables)

        php artisan migrate
        php -S localhost:8000 -t public

    ## Database seeding

    **Populate the database with seed data with relationships which includes users, articles, comments, tags, favorites and follows. This can help you to quickly start testing the api or couple a frontend and start using it with ready content.**

    Run the database seeder and you're done

        php artisan db:seed

    ***Note*** : It's recommended to have a clean database before seeding. You can refresh your migrations at any point to clean the database by running the following command

        php artisan migrate:refresh

    ## API Specification

    This application adheres to the api specifications set by the [Thinkster](https://github.com/gothinkster) team. This helps mix and match any backend with any other frontend without conflicts.

    > [Full API Spec](https://github.com/gothinkster/realworld/tree/master/api)

    More information regarding the project can be found here https://github.com/gothinkster/realworld

    ----------

    # Code overview

    ## Dependencies

    - [laravel-mongodb](https://github.com/jenssegers/laravel-mongodb) - MongoDB based Eloquent model and Query builder
    - [jwt-auth](https://github.com/tymondesigns/jwt-auth) - For authentication using JSON Web Tokens
    - [laravel-cors](https://github.com/barryvdh/laravel-cors) - For handling Cross-Origin Resource Sharing (CORS)

    ## Folders

    - `app/Models` - Contains all the Eloquent models
    - `app/Http/Controllers` - Contains all the api controllers
    - `app/Http/Middleware` - Contains the JWT auth middleware
    - `app/Providers` - Contains the JWT auth service provider
    - `app/RealWorld/Favorite` - Contains the files implementing the favorite feature
    - `app/RealWorld/Filters` - Contains the query filters used for filtering api requests
    - `app/RealWorld/Follow` - Contains the files implementing the follow feature
    - `app/RealWorld/Paginator` - Contains the pagination class used to paginate the result
    - `app/RealWorld/Slug` - Contains the files implementing slugs to articles
    - `config` - Contains all the application configuration files
    - `database/factories` - Contains the model factory for all the models
    - `database/migrations` - Contains all the database migrations
    - `database/seeds` - Contains the database seeder
    - `routes` - Contains all the api routes defined in web.php file
    - `tests` - Contains all the application tests
    - `tests/Feature/Api` - Contains all the api tests

    ## Environment variables

    - `.env` - Environment variables can be set in this file

    ***Note*** : You can quickly set the database information and other variables in this file and have the application fully working.

    ----------

    # Testing API

    Run the Lumen development server

        php -S localhost:8000 -t public

    The api can now be accessed at

        http://localhost:8000/api

    Request headers

    | **Required** 	| **Key**              	| **Value**            	|
    |----------	|------------------	|------------------	|
    | Yes      	| Content-Type     	| application/json 	|
    | Yes      	| X-Requested-With 	| XMLHttpRequest   	|
    | Optional 	| Authorization    	| Token {JWT}      	|

    Refer the [api specification](#api-specification) for more info.

    ----------

    # Authentication

    This applications uses JSON Web Token (JWT) to handle authentication. The token is passed with each request using the `Authorization` header with `Token` scheme. The JWT authentication middleware handles the validation and authentication of the token. Please check the following sources to learn more about JWT.

    - https://jwt.io/introduction/
    - https://self-issued.info/docs/draft-ietf-oauth-json-web-token.html

    ----------

    # Cross-Origin Resource Sharing (CORS)

    This applications has CORS enabled by default on all API endpoints. The CORS allowed origins can be changed by setting them in the config file. Please check the following sources to learn more about CORS.

    - https://developer.mozilla.org/en-US/docs/Web/HTTP/Access_control_CORS
    - https://en.wikipedia.org/wiki/Cross-origin_resource_sharing
    - https://www.w3.org/TR/cors
    
   resources
   +---- views
   routes
   +---- web.php

    <?php

    /*
    |--------------------------------------------------------------------------
    | Application Routes
    |--------------------------------------------------------------------------
    |
    | Here is where you can register all of the routes for an application.
    | It is a breeze. Simply tell Lumen the URIs it should respond to
    | and give it the Closure to call when that URI is requested.
    |
    */

    $router->get('/', function () {
        return response(file_get_contents(__DIR__ . '/../readme.md'))
                ->header('Content-Type', 'text/plain');
    });

    // Generate random string
    $router->get('appKey', function () {
        return str_random('32');
    });

    $router->group(['prefix' => 'api'], function ($router) {

        /**
        * Authentication
        */
        $router->post('users/login', 'AuthController@login');
        $router->post('users', 'AuthController@register');

        /**
        * Current user
        */
        $router->get('user', 'UserController@index');
        $router->put('user', 'UserController@update');

        /**
        * User profile
        */
        $router->group(['prefix' => 'profiles/{username}'], function ($router) {

            $router->get('/', 'ProfileController@show');
            $router->post('follow', 'ProfileController@follow');
            $router->delete('follow', 'ProfileController@unFollow');
        });

        /**
        * Articles
        */
        $router->get('articles', 'ArticleController@index');
        $router->post('articles', 'ArticleController@store');
        $router->get('articles/feed', 'ArticleController@feed');

        $router->group(['prefix' => 'articles/{slug:[a-z-]+}'], function ($router) {

            $router->get('/', 'ArticleController@show');
            $router->put('/', 'ArticleController@update');
            $router->delete('/', 'ArticleController@destroy');

            /**
            * Comments
            */
            $router->post('comments', 'CommentController@store');
            $router->get('comments', 'CommentController@index');
            $router->delete('comments/{id:[a-z0-9]+}', 'CommentController@destroy');

            /**
            * Favorites
            */
            $router->post('favorite', 'ArticleController@addFavorite');
            $router->delete('favorite', 'ArticleController@unFavorite');
        });

        /**
        * Tags
        */
        $router->get('tags', 'ArticleController@tags');
    });
    
   storage
   +---- app
   +---- framework
   |      +---- cache
   |      +---- views
   +---- logs
   tests
   +---- ArticleCreateTest.php

    <?php

    class ArticleCreateTest extends TestCase
    {
        /** @test */
        public function it_returns_the_article_on_successfully_creating_a_new_article()
        {
            $data = [
                'article' => [
                    'title' => 'test title',
                    'description' => 'test description',
                    'body' => 'test body with random text',
                    'tagList' => ['test', 'coding'],
                ]
            ];

            $response = $this->json('POST', '/api/articles', $data, $this->headers);

            $response->assertResponseStatus(201);
            $response->seeJsonStructure([
                    'article' => [
                        'slug',
                        'title',
                        'description',
                        'body',
                        'tagList',
                        'favorited',
                        'favoritesCount',
                        'author' => [
                            'username',
                            'bio',
                            'image',
                            'following',
                        ]
                    ]
            ]);
            $response->seeJsonContains(['title' => 'test title']);
            $response->seeJsonContains(['username' => $this->loggedInUser->username]);
        }

        /** @test */
        public function it_returns_appropriate_field_validation_errors_when_creating_a_new_article_with_invalid_inputs()
        {
            $data = [
                'article' => [
                    'title' => '',
                    'description' => '',
                    'body' => '',
                ]
            ];

            $response = $this->json('POST', '/api/articles', $data, $this->headers);

            // $response->assertResponseStatus(422);
            $response->seeJsonEquals([
                    'errors' => [
                        'title' => ['field is required.'],
                        'description' => ['field is required.'],
                        'body' => ['field is required.'],
                    ]
                ]);
        }

        /** @test */
        public function it_returns_an_unauthorized_error_when_trying_to_add_article_without_logging_in()
        {
            $data = [
                'article' => [
                    'title' => 'test title',
                    'description' => 'test description',
                    'body' => 'test body with random text',
                    'tags' => ['test', 'coding'],
                ]
            ];

            $response = $this->json('POST', '/api/articles', $data);

            $response->assertResponseStatus(401);
        }
    }
    
   +---- ArticleDeleteTest.php

    <?php

    class ArticleDeleteTest extends TestCase
    {
        /** @test */
        public function it_returns_a_200_success_response_on_successfully_removing_the_article()
        {
            $article = $this->loggedInUser->articles()->save(factory(\App\Models\Article::class)->make());

            $response = $this->json('DELETE', "/api/articles/{$article->slug}", [], $this->headers);

            $response->assertResponseStatus(204);

            $response = $this->json('GET', "/api/articles/{$article->slug}");

            $response->assertResponseStatus(404);
        }

        /** @test */
        public function it_returns_an_unauthorized_error_when_trying_to_remove_article_without_logging_in()
        {
            $article = $this->loggedInUser->articles()->save(factory(\App\Models\Article::class)->make());

            $response = $this->json('DELETE', "/api/articles/{$article->slug}");

            $response->assertResponseStatus(401);
        }

        /** @test */
        public function it_returns_a_forbidden_error_when_trying_to_remove_articles_by_others()
        {
            $article = $this->user->articles()->save(factory(\App\Models\Article::class)->make());

            $response = $this->json('DELETE', "/api/articles/{$article->slug}", [], $this->headers);

            $response->assertResponseStatus(403);
        }
    }
    
   +---- ArticlePaginateTest.php

    <?php

    class ArticlePaginateTest extends TestCase
    {
        public function setUp(): void
        {
            parent::setUp();

            $this->artisan('migrate:refresh');
            $this->artisan('db:seed');

            $users = factory(\App\Models\User::class)->times(2)->create();

            $this->loggedInUser = $users[0];

            $this->user = $users[1];

            $this->headers = [
                'Authorization' => "Token {$this->loggedInUser->token}"
            ];
        }

        /** @test */
        public function it_returns_the_correct_articles_with_limit_and_offset()
        {
            $response = $this->json('GET', '/api/articles');

            $response->assertResponseOk();
            $response->seeJsonContains(['articlesCount' => 25]);

            // $this->assertCount(20, $this->getResponseData()['articles'], 'Expected articles to set default limit to 20');

            $response = $this->json('GET', '/api/articles?limit=10&offset=5');

            $response->assertResponseOk();
            $response->seeJsonContains(['articlesCount' => 10]);

            // $this->assertCount(10, $this->getResponseData()['articles'], 'Expected article limit of 10 when set');
        }
    }
    
   +---- ArticleReadTest.php

    <?php

    class ArticleReadTest extends TestCase
    {
        /** @test */
        public function it_returns_the_articles_and_correct_total_article_count()
        {
            $articles = $this->user->articles()->saveMany(factory(\App\Models\Article::class)->times(2)->make());

            $response = $this->json('GET', '/api/articles');

            $response->assertResponseOk();
            $response->seeJsonStructure([
                    'articles' => [
                        [
                            'slug',
                            'title',
                            'description',
                            'body',
                            'tagList',
                            'createdAt',
                            'updatedAt',
                            'favorited',
                            'favoritesCount',
                            'author' => [
                                'username',
                                'bio',
                                'image',
                                'following',
                            ]
                        ],
                        [
                            'slug',
                            'title',
                        ]
                    ],
                    'articlesCount'
            ]);

            // $response->seeJsonContains(['articlesCount' => 2]);
            $response->seeJsonContains(['slug' => $articles[0]->slug]);
            $response->seeJsonContains(['slug' => $articles[1]->slug]);
            $response->seeJsonContains(['createdAt' => $articles[1]->created_at->toAtomString()]);
            $response->seeJsonContains(['username' => $this->user->username]);
        }

        /** @test */
        public function it_returns_the_article_by_slug_if_valid_and_not_found_error_if_invalid()
        {
            $article = $this->user->articles()->save(factory(\App\Models\Article::class)->make());

            $response = $this->json('GET', '/api/articles');

            $response->assertResponseOk();

            $response->seeJsonContains(['slug' => $article->slug]);
            $response->seeJsonContains(['title' => $article->title]);
            $response->seeJsonContains(['createdAt' => $article->created_at->toAtomString()]);
            $response->seeJsonContains(['username' => $this->user->username]);

            $response = $this->json('GET', '/api/articles/randominvalidslug');

            $response->assertResponseStatus(404);
        }
    }
    
   +---- HomePageTest.php

    <?php

    class HomePageTest extends TestCase
    {
        /**
        * A basic test example.
        *
        * @return void
        */
        public function testHomePage()
        {
            $this->get('/');

            $this->assertEquals(
                file_get_contents(__DIR__ . '/../readme.md'),
                $this->response->getContent()
            );
        }
    }
    
   +---- LoginTest.php

    <?php

    class LoginTest extends TestCase
    {
        /** @test */
        public function it_returns_a_user_with_valid_token_on_valid_login()
        {
            $data = [
                'user' => [
                    'email' => $this->user->email,
                    'password' => 'password',
                ]
            ];

            $response = $this->json('POST', '/api/users/login', $data);

            $response->assertResponseOk();
            $response->seeJsonStructure([
                'user' => [
                    'bio',
                    'email',
                    'image',
                    'token',
                    'username',
                ]
            ]);

            $response->seeJsonContains(['bio' => $this->user->bio]);
            $response->seeJsonContains(['email' => $this->user->email]);
            $response->seeJsonContains(['image' => $this->user->image]);
            $response->seeJsonContains(['username' => $this->user->username]);

            $responseData = $this->getResponseData();

            $this->assertArrayHasKey('token', $responseData['user'], 'Token not found');

            $this->assertTrue(
                (count(explode('.', $responseData['user']['token'])) === 3),
                'Failed to validate token'
            );
        }

        /** @test */
        public function it_returns_field_required_validation_errors_on_invalid_login()
        {
            $data = [];

            $response = $this->json('POST', '/api/users/login', $data);

            $response->assertResponseStatus(422);
            $response->seeJsonEquals([
                    'errors' => [
                        'email' => ['field is required.'],
                        'password' => ['field is required.'],
                    ]
                ]);
        }

        /** @test */
        public function it_returns_appropriate_field_validation_errors_on_invalid_login()
        {
            $data = [
                'user' => [
                    'email' => 'invalid email',
                    'password' => 'password',
                ]
            ];

            $response = $this->json('POST', '/api/users/login', $data);

            $response->assertResponseStatus(422);
            $response->seeJsonEquals([
                    'errors' => [
                        'email' => ['must be a valid email address.'],
                    ]
                ]);
        }
    }
    
   +---- RegistrationTest.php

    <?php

    class RegistrationTest extends TestCase
    {
        /** @test */
        public function it_returns_user_with_token_on_valid_registration()
        {
            $user = factory('App\Models\User')->make();
            $data = [
                'user' => [
                    'username' => $user->username,
                    'email' => $user->email,
                    'password' => $user->password,
                ]
            ];

            $response = $this->json('POST', '/api/users', $data);

            $response->assertResponseStatus(201);
            $response->seeJsonStructure([
                'user' => [
                    'email',
                    'username',
                ]
            ]);

            $this->assertArrayHasKey('token', $this->getResponseData()['user'], 'Token not found');
        }

        /** @test */
        public function it_returns_field_required_validation_errors_on_invalid_registration()
        {
            $data = [];

            $response = $this->json('POST', '/api/users', $data);

            $response->assertResponseStatus(422);
            $response->seeJsonEquals([
                    'errors' => [
                        'username' => ['field is required.'],
                        'email' => ['field is required.'],
                        'password' => ['field is required.'],
                    ]
                ]);
        }

        /** @test */
        public function it_returns_appropriate_field_validation_errors_on_invalid_registration()
        {
            $data = [
                'user' => [
                    'username' => 'invalid username',
                    'email' => 'invalid email',
                    'password' => '1',
                ]
            ];

            $response = $this->json('POST', '/api/users', $data);

            $response->assertResponseStatus(422);
            $response->seeJsonEquals([
                    'errors' => [
                        'username' => ['may only contain letters and numbers.'],
                        'email' => ['must be a valid email address.'],
                        'password' => ['must be at least 8 characters.'],
                    ]
                ]);
        }

        /** @test */
        public function it_returns_username_and_email_taken_validation_errors_when_using_duplicate_values_on_registration()
        {
            $data = [
                'user' => [
                    'username' => $this->user->username,
                    'email' => $this->user->email,
                    'password' => $this->user->password,
                ]
            ];

            $response = $this->json('POST', '/api/users', $data);

            $response->assertResponseStatus(422);
            $response->seeJsonEquals([
                    'errors' => [
                        'username' => ['has already been taken.'],
                        'email' => ['has already been taken.'],
                    ]
                ]);
        }
    }
    
   +---- TagTest.php

    <?php

    class TagTest extends TestCase
    {
        /** @test */
        public function it_returns_an_array_of_tags()
        {
            $response = $this->json('GET', '/api/tags');

            $response->assertResponseOk();
            $response->shouldReturnJson();
        }
    }
    
   +---- TestCase.php

    <?php

    abstract class TestCase extends Laravel\Lumen\Testing\TestCase
    {
        // protected $baseUrl = 'http://realworld.test:8080';

        protected $loggedInUser;

        protected $user;

        protected $headers;

        protected static $migrationsRun = false;

        /**
        * Creates the application.
        *
        * @return \Laravel\Lumen\Application
        */
        public function createApplication()
        {
            return require __DIR__ . '/../bootstrap/app.php';
        }


        public function setUp(): void
        {
            parent::setUp();

            if (!static::$migrationsRun) {
                $this->artisan('migrate:refresh');
                $this->artisan('db:seed');
                static::$migrationsRun = true;
            }

            $this->beforeApplicationDestroyed(function () {
                // $this->artisan('migrate:rollback');
            });

            $users = factory(\App\Models\User::class)->times(2)->create();

            $this->loggedInUser = $users[0];

            $this->user = $users[1];

            $this->headers = [
                'Authorization' => "Token {$this->loggedInUser->token}"
            ];
        }

        /**
        * Get the JSON data from the response and return as assoc. array
        *
        * @return array
        */
        public function getResponseData()
        {
            return json_decode(json_encode($this->response->getData()), true);
        }
    }
    

Back to Main Page