Book Image

Mastering Laravel

By : Christopher Pecoraro
Book Image

Mastering Laravel

By: Christopher Pecoraro

Overview of this book

<p>PHP continues to revive and Laravel is at its forefront. Laravel follows modern PHP's object-oriented best practices and reduces time-to-market, enabling you to build robust web and API-driven mobile applications that can be automatically tested and deployed.</p> <p>With this book you will learn how to rapidly develop software applications using the Laravel 5 PHP framework.</p> <p>This book walks you through the creation of an application, starting with behavior-driven design of entities. You'll explore various aspects of modern software including the RESTful API, and will be introduced to command bus. Laravel's annotations package is also explained and demonstrated. Finally, the book closes with a demonstration of different ways to deploy and scale your applications.</p>
Table of Contents (17 chapters)
Mastering Laravel
Credits
About the Author
About the Reviewers
www.PacktPub.com
Preface
3
Building Services, Commands, and Events
Index

Specifying with phpspec


At the core of phpspec is the ability to allow us to specify the behavior of entities and simultaneously test them. By simply specifying what the business rules are as given by the customer, we can easily create tests for each business rule. However, the real power of phpspec lies in how it uses an expressive, natural language syntax. Let's take a look at the business rules that were previously given to us regarding reservations:

  • The start date of the reservation must come before the end date

  • A reservation cannot be made for more than fifteen days

  • A reservation cannot include more than four rooms

Run the following command:

# phpspec describe
 MyCompany/Accommodation/ReservationValidator

phpspec will produce the following output for the preceding command:

<?php

namespace spec\MyCompany\Accommodation;

use PhpSpec\ObjectBehavior;
use Prophecy\Argument;

class ReservationSpec extends ObjectBehavior
{
    function it_is_initializable()
    {
        $this->shouldHaveType('MyCompany\Accommodation\Reservation');
    }
}

Then, run phpspec by using the following command:

# phpspec run

phpspec will respond as usual with the following output:

Do you want me to create 
  'MyCompany\Accommodation\ReservationValidator' for you?

Then, phpspec will create the ReservationValidator class, as follows:

<?php namespace MyCompany\Accommodation;

 class ReservationValidator {
 }

Let's create a validate() function that will take the following parameters:

  • A start date string that determines the start of the reservation

  • An end date string that determines the end of the reservation

  • An array of room objects to add to the reservation

The following is the code snippet that creates the validate() function:

<?php
namespace MyCompany\Accommodation;

use Carbon\Carbon;

class ReservationValidator
{
    
    public function validate($start_date, $end_date, $rooms)
    {
    }
}

We will include the Carbon class, which will help us to work with the dates. For the first business rule, which states that the start date of the reservation must come before the end date, we can now create our first specification method in the ReservationValidatorSpec class, as follows:

function its_start_date_must_come_before_the_end_date ($start_date,$end_date,$room)
{
    $rooms = [$room];
    $start_date = '2015-06-03';
    $end_date = '2015-06-03';
    $this->shouldThrow('\InvalidArgumentException')->duringValidate( $start_date, $end_date, $rooms);
}

In the preceding function, phpspec starts the specification with it or its. phpspec uses the snake case for high legibility, and start_date_must_be_less_than_the_end_date is an exact copy of the specification. Isn't this just wonderful?

When $start_date, $end_date, and the room are passed, they automatically get mocked. Nothing else is needed. We will create a $rooms array that is valid. However, we will set the $start_date and $end_date in such a way that they both have the same values to cause the test to fail. The expression syntax is shown in the preceding code. The shouldThrow comes before during, which then takes the method name Validate.

We have given phpspec what it needs to automatically create the validate() method for us. We will specify that $this, which is the ReservationValidator class, will throw an InvalidArgumentException. Run the following command:

# phpspec run

Once again, phpspec asks us the following:

 Do you want me to create 'MyCompany\Accommodation\Reservation::validate()'  
  for you?

By simply typing Y at the prompt, the method is created inside the ReservationValidator class. It is that easy. When phpspec is run again, it will fail because the method has not thrown an exception yet. So now, the code needs to be written. Inside the function, we will create two Carbon objects from a string that is formatted like "2015-06-02" so that we are able to harness the power of Carbon's powerful date comparisons. In this case, we will use the $date1->diffInDays($date2); method to test whether the difference between the $end and the $start is less than one. If this is the case, we will throw the InvalidArgumentException and display a user-friendly message. Now, when we rerun phpspec, the test will pass:

$end = Carbon::createFromFormat('Y-m-d', $end_date);
$start = Carbon::createFromFormat('Y-m-d', $start_date);

        if ($end->diffInDays($start)<1) {
            throw new \InvalidArgumentException('Requires end date to be greater than start date.');
        }

Red, green, refactor

The rules of test-driven development call for red, green, refactor, which means that once the tests pass (green), we should try to refactor or simplify the code inside the method without altering the functionality.

Have a look at the if test:

if ( $end->diffInDays($start) < 1 ) {

The preceding code isn't quite readable. We can refactor it in the following way:

if (!$end->diffInDays($start)>0)

However, even the preceding code is not very legible, and we are also using an integer directly in the code.

Let's move 0 into a constant. To improve the readability, we'll change it to the minimum amount of days required for a reservation, as follows:

 const MINIMUM_STAY_LENGTH = 1;

Let's extract the comparison into a method, as follows:

    /**
     * @param $end
     * @param $start
     * @return bool
     */
    private function endDateIsGreaterThanStartDate($end, $start)
    {
        return $end->diffInDays($start) >= MINIMUM_STAY_LENGTH;
    }

We can now write the if statement like this:

if (!$this->endDateIsGreaterThanStartDate($end, $start))

The preceding statement is much more expressive and readable.

Now, for the next rule, which states that a reservation cannot be made for more than fifteen days, we'll need to create the method in the following way:

function it_cannot_be_made_for_more_than_fifteen_days(User $user, $start_date, $end_date, Room $room)
{
        $start_date = '2015-06-01';
        $end_date = '2015-07-30';
        $rooms = [$room];
        $this->shouldThrow('\InvalidArgumentException')
        ->duringCreateNew( $user,$start_date,$end_date,$rooms);
}

Here, we set the $end_date so that it is assigned a date that occurs more than a month after the $start_date to cause the method to throw an InvalidArgumentException. Once again, upon execution of the phpspec command, the test will fail. Let's modify the existing method to check the date range. We'll add the following code to the method:

  if ($end->diffInDays($start)>15) {
       throw new \InvalidArgumentException('Cannot reserve a room
       for more than fifteen (15) days.');
  }

Once again, phpspec happily runs all the tests successfully. Refactoring, we will once again extract the if condition and create the constant, as follows:

   const MAXIMUM_STAY_LENGTH = 15;
   /**
     * @param $end
     * @param $start
     * @return bool
     */
    private function daysAreGreaterThanMaximumAllowed($end, $start)
    {
        return $end->diffInDays($start) > self::MAXIMUM_STAY_LENGTH;
    }

   if ($this->daysAreGreaterThanMaximumAllowed($end, $start)) {
            throw new \InvalidArgumentException ('Cannot reserve a room for more than fifteen (15) days.');
   }

Tidying things up

We could leave things like this, but let's clean it up since we have tests. Since the endDateIsGreaterThanStartDate($end, $start) and daysAreGreaterThanMaximumAllowed($end, $start) functions both check for the minimum and maximum allowed stay respectively, we can call them from another method.

We will refactor endDateIsGreaterThanStartDate() into daysAreLessThanMinimumAllowed($end, $start) and then create another method that checks both the minimum and maximum stay length, as follows:

private function daysAreWithinAcceptableRange($end, $start)
    {
        if ($this->daysAreLessThanMinimumAllowed($end, $start)
            || $this->daysAreGreaterThanMaximumAllowed($end, $start)) {
           return false;
        } else {
           return true;
        }
    }

This leaves us with simply one function, instead of two, in the createNew function, as follows:

if (!$this->daysAreWithinAcceptableRange($end, $start)) {
            throw new \InvalidArgumentException('Requires a stay length from '
                . self::MINIMUM_STAY_LENGTH . ' to '. self::MAXIMUM_STAY_LENGTH . ' days.');
        }

For the third rule, which states that a reservation cannot contain more than four rooms, the process is the same. Create the specification, as follows:

it_cannot_contain_than_four_rooms

The change here will be in the parameters. This time, we will mock five rooms so that the test will fail, as follows:

function it_cannot_contain_than_four_rooms(User $user, $start_date, $end_date, Room $room1, Room $room2, Room $room3, Room $room4, Room $room5)

Five room objects will be loaded into the $rooms array, and the test will fail as follows:

$rooms = [$room1, $room2, $room3, $room4, $room5];
    $this->shouldThrow('\InvalidArgumentException')->duringCreateNew($user,$start_date,$end_date,$rooms);
    }

After adding code to check the size of the array, the final class will look like this:

<?php

namespace MyCompany\Accommodation;

use Carbon\Carbon;
class ReservationValidator
{

    const MINIMUM_STAY_LENGTH = 1;
    const MAXIMUM_STAY_LENGTH = 15;
    const MAXIMUM_ROOMS = 4;

    /**
     * @param $start_date
     * @param $end_date
     * @param $rooms
     * @return $this
     */
    public function validate($start_date, $end_date, $rooms)
    {
        $end = Carbon::createFromFormat('Y-m-d', $end_date);
        $start = Carbon::createFromFormat('Y-m-d', $start_date);

        if (!$this->daysAreWithinAcceptableRange($end, $start)) {
            throw new \InvalidArgumentException('Requires a stay length from '
                . self::MINIMUM_STAY_LENGTH . ' to '. self::MAXIMUM_STAY_LENGTH . ' days.');
        }
        if (!is_array($rooms)) {
            throw new \InvalidArgumentException('Requires last parameter rooms to be an array.');
        }
        if ($this->tooManyRooms($rooms)) {
            throw new \InvalidArgumentException('Cannot reserve more than '. self::MAXIMUM_ROOMS .' rooms.');
        }

        return $this;

    }

    /**
     * @param $end
     * @param $start
     * @return bool
     */
    private function daysAreLessThanMinimumAllowed($end, $start)
    {
        return $end->diffInDays($start) < self::MINIMUM_STAY_LENGTH;
    }

    /**
     * @param $end
     * @param $start
     * @return bool
     */
    private function daysAreGreaterThanMaximumAllowed($end, $start)
    {
        return $end->diffInDays($start) > self::MAXIMUM_STAY_LENGTH;
    }

    /**
     * @param $end
     * @param $start
     * @return bool
     */
    private function daysAreWithinAcceptableRange($end, $start)
    {
        if ($this->daysAreLessThanMinimumAllowed($end, $start)
            || $this->daysAreGreaterThanMaximumAllowed($end, $start)) {
            return false;
        } else {
            return true;
        }
    }

    /**
     * @param $rooms
     * @return bool
     */
    private function tooManyRooms($rooms)
    {
        return count($rooms) > self::MAXIMUM_ROOMS;
    }

    public function rooms(){
        return $this->belongsToMany('MyCompany\Accommodation\Room')->withTimestamps();
    }

}

The method is very clean. There are only two if statements—the first to verify that the date range is valid, and other one to verify that the number of rooms is within the valid range. The constants are easily accessible and can be changed as the business requirements change. Clearly, the addition of phpspec into the development workflow combines what earlier required two steps—writing the assertions with PHPUnit and then writing the code. Now, we will leave phpspec and move on to Artisan, which developers are familiar with as it was a feature of the previous versions of Laravel.