Introducing the Yii framework into our application

Now that we have the entire supporting infrastructure we need to begin working with, let's return to our first feature we defined at the design stage and define the acceptance test for it.

First end-to-end test

The main point with end-to-end acceptance tests is that we have to deal with our application using only its UI. We don't have any way of direct access to the database or, worse, filesystem around the application. So, to test a query for some data in the database, this data should be inserted into the database first. And it should be done using the UI.

Here are the resulting test steps:

  1. Open the UI for adding customer data to the database.
  2. Add customer #1 to the database. You should see the Customer List UI with one record.
  3. Add customer #2 to the database. You should see the Customer List UI with two records now.
  4. Open the UI to query customer data by the phone number.
  5. Query using the phone number of customer #1. You should see the query results UI with data for customer #1 but not for customer #2.

So, this test forces us to have three user-interface pages: the new customer records, the customer list, and the query UI. That's part of why it's called "end-to-end" testing.

Translated to the Codeception acceptance test suite, the procedure just described will look like this:

$I = new AcceptanceTesterCRMOperatorSteps($scenario);
$I->wantTo('add two different customers to database'),

$I->amInAddCustomerUi();
$first_customer = $I->imagineCustomer();
$I->fillCustomerDataForm($first_customer);
$I->submitCustomerDataForm();

$I->seeIAmInListCustomersUi();

$I->amInAddCustomerUi();
$second_customer = $I->imagineCustomer();
$I->fillCustomerDataForm($second_customer);
$I->submitCustomerDataForm();

$I->seeIAmInListCustomersUi();

$I = new AcceptanceTesterCRMUserSteps($scenario);
$I->wantTo('query the customer info using his phone number'),

$I->amInQueryCustomerUi();
$I->fillInPhoneFieldWithDataFrom($first_customer);
$I->clickSearchButton();

$I->seeIAmInListCustomersUi();
$I->seeCustomerInList($first_customer);
$I->dontSeeCustomerInList($second_customer);

Let's put it as is inside the tests/acceptance/QueryCustomerByPhoneNumberCept.php file. This is our goal for the current chapter.

Let's go over the not-so-obvious things in this test script.

First, we broke down the scenario into two logical parts and made two different subclasses of AcceptanceTester to stress this distinction. Codeception has a nice helper to automatically generate subclasses of different Guy classes, using which we created the AcceptanceTesterCRMOperatorSteps class as shown in the following command:

$ cept generate:stepobject acceptance CRMOperatorSteps

Composer will prompt you for the names of the methods in the step where the object class is being generated. Just hit Enter there without writing anything to tell it that you want to start afresh.

This helper is used to support the StepObject pattern (see http://codeception.com/docs/07-AdvancedUsage#StepObjects), so it'll automatically append the Steps suffix to the CRMOperatorSteps class name. Of course, it's a lot more natural to reason about the subclasses of AcceptanceTester as having different roles than having some abstract containers of steps. However, if we forcefully rename the generated classes, removing the unnecessary suffix, we would lose the auto-loading ability that Codeception automatically provides us with, so we'll just tolerate it. The preceding incantation places the CRMOperatorSteps.php class file into the tests/acceptance/_steps subdirectory.

In the same way, the CRMUserSteps class can be generated.

Now, let's define the steps mentioned in the test scenario. Almost all of these high-level steps will just be the containers of the more low-level steps built-in with the acceptance tester shipped with Codeception.

First, we will see the CRM operator steps.

The "I am in Add Customer UI" step will just be an opening of the route corresponding to our future Add Customer UI, so it'll look like this:

    function amInAddCustomerUi()
    {
        $I = $this;
        $I->amOnPage('/customers/add'),
    }

"Imagine Customer" is a helper to generate customer data to be entered in the Add Customer UI automatically and in a random way. Such placeholder data can be generated in any way. We'll be using the amazing Faker library (https://github.com/fzaninotto/Faker) to generate the correct-looking data for us. However, we'll look into that a bit later. Right now, the need to enter data in the Add Customer UI forces us to decide on the actual UI. We don't go after any fancy interface here; it'll be just a plain HTML form with the Submit button. But what fields should be there? Let's return to our Customer aggregate and see what parts of it we crucially need to fulfill our test scenario:

First end-to-end test

For simplicity, we leave off the E-mail and Address models for later. Of course, we don't take into account the Contract aggregate at all. For the task to be any "interesting", we include all the unique parts of the customer: Name, Birthday, and Notes. Do remember that Name is a structure and not just a line of text like Notes.

Now, let's settle on the fields in our Add Customer form. Please pay attention to the naming of form fields; it's not arbitrary and corresponds to our future database schema and the Yii 2 model's configuration. Have a look at the following table:

Field

Name in form

Full Name

CustomerRecord[name]

Birth Date

CustomerRecord[birth_date]

Notes

CustomerRecord[notes]

Phone Number

PhoneRecord[number]

Note that while in our design one customer can have several phones, here we allow only one. We cannot implement features until we have a basic test done for it, and our test does not explicitly check the ability to enter several phone numbers (yet).

So, we can now define the CRMOperatorSteps.imagineCustomer method. First of all, let's bring the Faker library into our project:

$ php composer.phar require "fzaninotto/faker:*"

Then, let's consider a customer with the attributes defined in the following method:

    public function imagineCustomer()
    {
        $faker = FakerFactory::create();
        return [
            'CustomerRecord[name]' => $faker->name,
            'CustomerRecord[birth_date]' => $faker->date('Y-m-d'),
            'CustomerRecord[notes]' => $faker->sentence(8),
            'PhoneRecord[number]' => $faker->phoneNumber
        ];
    }

Our imagination here makes a structure for us, which we can easily use in our fillCustomerData method:

    function fillCustomerDataForm($fieldsData)
    {
        $I = $this;
        foreach ($fieldsData as $key => $value)
            $I->fillField($key, $value);
    }

The method to submit the form will be straightforward; let's just name the button Submit:

    function submitCustomerDataForm()
    {
        $I = $this;
        $I->click('Submit'),
    }

Then, we need only two methods, one for checking whether we are inside the Customer List UI and another to actually go there:

    public function seeIAmInListCustomersUi()
    {
        $I = $this;
        $I->seeCurrentUrlMatches('/customers/'),
    }

function amInListCustomersUi()
    {
        $I = $this;
        $I->amOnPage('/customers'),
    }

In the Codeception ideology, assertion methods are expected to have the see prefix on the names, so we adhere to it.

We use CurrentUrlMatches to match URLs using regular expressions instead of the more strict CurrentUrlEquals, because we assume there'll be some query parameters at the end of the URL.

With all these methods defined in the CRMOperatorSteps class, we now have the first half of our test case completed (which means runnable).

Let's get done with the test steps for a CRM user, who'll do the querying. The following changes are to be done in the CRMUserSteps class. First, the obvious one is as follows:

    function amInQueryCustomerUi()
    {
        $I = $this;
        $I->amOnPage('/customers/query'),
    }

Let's just name the field to enter the phone number in the same way we named it in the Add Customer form as follows:

    function fillInPhoneFieldWithDataFrom($customer_data)
    {
        $I = $this;
        $I->fillField(
            'PhoneRecord[number]', 
            $customer_data['PhoneRecord[number]']
        );
    }

Let's name the button to start searching for customer data as the Search button as follows:

    function clickSearchButton()
    {
        $I = $this;
        $I->click('Search'),
    }

Then, we arrive at the duplication of CRMOperatorSteps.seeIAmInListCustomersUi:

    function seeIAmInListCustomersUi()
    {
        $I = $this;
        $I->seeCurrentUrlMatches('/customers/'),
    }

Let's just stick to the Rule of Three proposed in Refactoring: Improving the Design of Existing Code, Martin Fowler, Kent Beck, John Brant, William Opdyke, and Don Roberts, Addison-Wesley Professional, and let this method be as it is.

Lastly, here are our assertions:

    function seeCustomerInList($customer_data)
    {
        $I = $this;
        $I->see($customer_data['CustomerRecord[name]'], '#search_results'),
    }
    function dontSeeCustomerInList($customer_data)
    {
        $I = $this;
        $I->dontSee($customer_data['CustomerRecord[name]'], '#search_results'),
    }

We should note that this is an extremely simple implementation, and it relies on several assumptions, which will be held true at this stage of development:

  • All customers have a name defined
  • No customers have identical names
  • Search results are being rendered inside the HTML element with the ID search_results

Let's keep this test as it is for simplicity, but when we have more than one search result, we should think about how to properly check for a particular search result's existence (and most probably, the default semantics of the see method will not be enough for us).

Quite an important question is why we don't check for the customer data being shown in the Customer List UI after each addition of a new customer. After we query for the phone number, we end up in the same Customer List UI after all.

Well, for now, the reasoning is dead simple: our goal is to check that we can query for a customer by the phone number. Also, the existence of some assertions halfway through the test case would violate the Single Assertion principle (explained in detail in Clean Code, Robert Martin, Prentice Hall). However, as this is the end-to-end acceptance test, doing this is probably not such a bad thing. Anyway, nothing prevents us from extending this test further (again, it's an acceptance test imitating the behavior of a real user), but for now, let's stick with this simple scenario.

If you run the completed test scenario now, you should get the following error message:

1) Failed to add two different customers to database in QueryCustomerByPhoneNumberCept
Sorry, I couldn't fill field "CustomerRecord[first_name]","Cheyanne":                            
Field by name, label, CSS or XPath 'CustomerRecord[first_name]' was not found on page.           
                                                                                           
Scenario Steps:                                                                            
2. I fill field "CustomerRecord[first_name]","Cheyanne"                                          
1. I am on page "/customers/add"

We get this error because we don't have the HTML form served for the /customers/add request.

We have finally arrived at the Yii 2 installation procedure.

Yii 2 installation to the bare code base

We're going to make a totally custom application, which will not have the intention to rely much on the directory structure conventions of the Yii framework, instead just using its classes where it's convenient.

First of all, you need to declare Yii 2 as a dependency of your application.

Either add the required require line for Yii 2 manually in the composer.json file or run the following command:

$ php composer.phar require "yiisoft/yii2:*"

If you edited the manifest manually, do not forget to call the installation command:

$ php composer.phar install

Composer will bring Yii 2 to your code base after that. It should end inside the vendor/yiisoft/yii2 folder.

Checking the requirements

Yii 2 includes an important feature, which is a built-in requirement checker. When you install one of the application templates discussed in Chapter 1, Getting Started, you get a script named requirements.php in the root of your code base. It's very useful, so make a copy and place it into the web subfolder. You can download this file from the Yii 2 repository at https://github.com/yiisoft/yii2/blob/master/apps/basic/requirements.php. After you get the file, you can run it from the command line by calling the following:

$ php web/requirements.php

Alternatively, you can just point your browser at the URL http://<your_domain>/requirements.php and get a nicely laid-out page with detailed explanations about whether your deploy target satisfies the framework requirements.

An introduction to Yii conventions

A really high-level explanation of things is as follows. To serve any of the requests coming to the application, Yii uses a single physical PHP script that instantiates a special object of the yiiwebApplication class. This object uses Yii's interpretation the of Model View Controller (MVC) composite pattern to process the request and display the result back to the sender. If you forgot or were unaware of the MVC before, then you probably want to read at least the official documentation for Yii for an in-depth explanation.

The Yii interpretation of MVC is as follows:

  • View is a class that does the rendering of whatever you send to the client. Usually, it is the HTML page, but you are not restricted to this
  • Model is a class that contains all the business logic
  • Controller is a class that receives the user request, decides what to do with it, calls models if necessary to do the real work, and then uses a view to render and send the result back to the user

The most subtle part of this scheme is the model concept. Depending on the interpretations, a model is either what the controller uses to get data to put into a view or actually is what the controller puts into a view. Yii 2 does not enforce any approach, but its implementation of models assumes that the model is a container for some data, either transient (in-memory only) or persistent (with the support of the active record pattern).

So, a request passes through the following steps:

  1. The web server receives the request and passes it to the index.php script.
  2. A Yii Application object is created. It decides which Controller class should be used to handle this request.
  3. A Controller object is created. It decides what Action it should run (they can be either methods of Controller or separate classes), and run it, passing the request details there.
  4. An action is performed, and, if it were properly constructed by the programmer, it returns something rendered by the view. It's not enforced by the framework in any way; you can have controller actions that do not render anything.
  5. A special application component is responsible for formatting data before sending it back to the user.
  6. Resulting data, be it HTML, JSON, XML, or an empty response, is sent to the user.

Knowing these steps, let's modify our current entry script, so it'll render the same thing, but using the Yii framework instead of displaying a raw text output. We'll look at a nice flowchart with all the details in Chapter 12, Route Management.

Building the wireframe code

Right now, we should have the following project structure:

Building the wireframe code

We will start by introducing Yii 2 from the entry point script. At a reasonable minimum, the index.php file should look like this:

<?php
// Including the Yii framework itself (1)
require(__DIR__ . '/../vendor/yiisoft/yii2/Yii.php'),
// Getting the configuration (2)
$config = require(__DIR__ . '/../config/web.php'),
// Making and launching the application immediately (3)
(new yiiwebApplication($config))->run();

At (1) in the code, we bring all that Yii needs to function properly to our environment.

At (2) in the code, we get the configuration tree for the application. Yii application config is a massive PHP array describing the initial values of attributes of the application itself as well as its various components.

At (3) in the code, we make a new instance of the Application subclass representing web applications and immediately call the method called run on it.

At (2) in the code we are loading a nonexistent file config/web.php. Let's make it:

<?php
return [
    'id' => 'crmapp',
    'basePath' => realpath(__DIR__ . '/../'),
    'components' => [
        'request' => [
            'cookieValidationKey' => 'your secret key here',
        ],
    ],];

We must specify three settings:

  • id: This is a mandatory identifier for our application. It is required because later we can split our application into different so-called modules, which we refer to using these IDs, and the top-level application obeys the same rules as a common module.
  • basePath: This is mandatory because it's basically the way for Yii to understand where it exists in the host filesystem. All relative paths allowed in other settings should start from here.
  • components.request.cookieValidationKey: This is a leak from the user authentication subsystem, which we will discuss in Chapter 5, User Authentication. This setting is the private key for validating users using the remember me feature, which relies on cookies. In earlier beta versions of Yii 2, this key was generated automatically. It was made visible by the 4e4e76e8 commit (https://github.com/yiisoft/yii2/commit/4e4e76e8838cbe097134d6f9c2ea58f20c1deed6). Apart from this setting, you can set components.request.enableCookieValidation to false, thus disabling cookie-based authentication completely. This will get your application working, too.

Next, we'll add some mandatory folders, because Yii just throws exceptions in case they are not there, and doesn't create them itself: web/assets and runtime. These folders are used by the framework when the application is running.

Adding a controller

Each controller class should have three distinct qualities:

  • It must belong to the namespace defined in the controllerNamespace setting of the Application class.
  • It must have a Controller suffix in the name.
  • It must extend the yiiaseController class. In the case of the controllers that are meant to be used by the web application and not the console one, we should extend the yiiwebController class instead.

In addition, it's important to understand how Yii 2 will actually search for controller classes.

Yii 2, in general, utilizes the autoloader compatible with the PSR-4 standard (http://www.php-fig.org/psr/psr-4/). To put it simply, such an autoloader treats namespaces as paths in the filesystem, given that there is a special root namespace defined, which maps to the root folder of the code base.

In our case, Yii 2 defines the app namespace for us, which maps to the root folder of the code base. As a result, for example, the default value of the controllerNamespace setting of the application, which is appcontrollers, will map to the directory called controllers at the root of code base, so all controller class definitions must be placed in there.

Also, each class to be available through the Yii 2 autoloader has to be put into its own file named exactly like the class itself.

So, let's create our first controller to satisfy the smoke test. We will not change the default controller namespace setting, and so we will write the following piece of code in the controllers/SiteController.php file:

namespace appcontrollers;
use yiiwebController;
class SiteController extends Controller
{
    public function actionIndex()
    {
        return 'Our CRM';
    }
}

This code heavily relies on the conventions of Yii. Without delving deep into the topic of routing in Yii, we can say that without special settings defined, Yii uses the actionIndex method of the controller named SiteController for the "/" request.

The most basic and straightforward way to define controller actions is to define them as public methods of the controllers having the name prefixed by action. To get into the SiteController.actionIndex method explicitly, you should request site/index.php.

So, we have our smoke test passing with the Yii-based routing. Let's add some helpers for ease of debugging.

Handling possible errors

You can get a lot of strange errors at this stage of development. Let's look at the ways you can use to set up your application quickly and gather as much feedback as possible.

First of all, when you make a really dire mistake, such as not defining id or basePath in the application config, you basically get a blank page as a response from the Yii application. The only place you can look at is the logs of your web server. For example, in Apache you can use the ErrorLog directive to specify the file in which such game-breaking error reports will end. Of course, any other application error will end there, regardless of whether it was rendered in the browser.

To fight off the "white screen," you can add a force override display_errors setting in your index.php entry point right after you require the Yii library, but before the creation and execution of the Application object as follows:

ini_set('display_errors', true);

Also, you should add one handy constant before you require the Yii library. It's extremely important to define it before you require the Yii library, because the Yii library will define it in the event that you don't define it yourself. That's it. Here's how you do it:

define('YII_DEBUG', true);

This will change the application to debug mode, and if some nasty exceptions are thrown, you'll get the generic status 500 page or blank screen but also a detailed report from Yii with the most important lines highlighted.

Lastly, you can add a custom logger to your application, which will log the errors happening within the application to a file. Chapter 8, Overall Behavior, explains this in great detail.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
3.137.176.166