PHP intermediate tutorial

In PHP beginners tutorial we saw basic stuff on how to declare and use DSL and PHP. This tutorial will explain some advanced techniques like history management, indexing search and creating reports.

Next change request that comes our way is that we want to track player changes, team switches and number changes. For a particular player we would like to see what teams did he play and when and what numbers did he have. We will create TeamHistory aggregate root which will contain all fields of aggregate Team and one additional field called createdAt.

module Football
{
    root Team
    {
        string name { unique; }
        detail players Player.team;
        date founded;
        specification searchByName 't => t.name == name'
        {
            string name;
        }
    }
    root TeamHistory
    {
        string name;
        detail players Player.team;
        date founded;
        date createdAt;
    }
    root Player
    {
        string name;
        string? nickname;
        Team? *team;
        int number;
    }
}

When using this objects in PHP, every time we change object Team we must reflect that change in TeamHistory. We could try to improve our DSL by removing all fields in aggregate Team and adding reference to Team object.

module Football
{
    root Team
    {
        string name { unique; }
        detail players Player.team;
        date founded;
        specification searchByName 't => t.name == name'
        {
            string name;
        }
    }
    root TeamHistory
    {
       Team *team; //although we don't want reference
       data createdAt;
    }
    root Player
    {
        string name;
        string? nickname;
        Team? *team;
        int number;
    }
}

Problem with this approach is that team field in TeamHistory is only reference to Team object and we need a deep copy of Team object. Also, if we decide to create history for object Player we would need to create PlayerHistory aggregate root. Recognize that this is a common pattern that we re-implement all the time. Fortunately DSL is an excellent tool to specify reoccurring patterns. In this case there exists a HISTORY keyword in DSL just for this usage.

module Football
{
    root Team
    {
        string name { unique; }
        detail players Player.team;
        date founded;
        specification searchByName 't => t.name == name'
        {
            string name;
        }
        history;
    }
    root Player
    {
        string name;
        string? nickname;
        Team? *team;
        int number;
        history;
    }
}

We've also added history to team object because we recognized that teams can change their name. Let's explore how history management is working:

$player1 =  new Player();
$player1->name = 'Chris';
$player1->persist(); // inserting player

$player1->name = 'Mark';
$player1->persist(); // changing player name to Mark

$player1->number = 9;
$player1->persist(); // changing player number to 9

$history = Player::getHistory($player1->URI);

foreach ($history as $h)
    echo $h->action . ' ' . $h->at . ' ' . $h->value->name . '<br>';

Output would be something like:

INSERT 2012-10-11T16:09:11.321000+02:00 Chris
UPDATE 2012-10-11T16:09:11.358000+02:00 Mark
UPDATE 2012-10-11T16:09:11.393000+02:00 Mark

GetHistory method finds all changes on some aggregate root which has HISTORY keyword. Result is field action, field at and field value which contains Player object at current time. If we need to get history from multiple players we could do so:

$history = Player::getHistory(array($player1->URI, $player2->URI, $player3->URI));

Let's provide fast search function by player nickname. Fast searching can be done with INDEX keyword, which will optimize searching for some specifications.

module Football
{
    root Team
    {
        string name { unique; }
        detail players Player.team;
        date founded;
        specification searchByName 't => t.name == name'
        {
            string name;
        }
        history;
    }
    root Player
    {
        string name;
        string? nickname;
        Team? *team;
        int number;
        index (nickname) where 'p => p.nickname != null';
        specification searchByNickname 'p => p.nickname == search'
        {
            string search;
        }
        history;
    }
}

In our case we've indexed only players which have nickname defined. We could also define index for every player, but since we are interested only in players with nicknames, we've optimized our index. Let's get back to our change request and finish implementing it. How will we expose single function for player history, by its nickname?

function getHistoryByNickname($nickname)
{
    $players = Player::searchByNickname($nickname);
    $uris = array();
    foreach ($players as $p)
        $uris[] = $p->URI;
    return Player::getHistory($uris);
}

$history = getHistoryByNickname('Speedo Gonzalo'); // first try changing and persisting some players nicknames

foreach ($history as $snapshots)
    foreach ($snapshots as $s)
        echo $s->action . ' ' . $s->at . ' ' . $s->value->name . '<br>';

As we can see, when getting history for multiple URIs, result is array of histories for players. In other words, we get {history for player1, history for player2, ..., history for playerN} where player1 to playerN are players with nickname Speedo Gonzalo.

Clients are pretty satisfied with our PHP app, but now they want to create PDF reports of Team card. They want to see Team info with list of current players.

For this task, we shall create a template in Word using Templater library which will populate data we send to it. But we don't want to fetch required Team and then provide it, we want to provide a team name and let the Platform find the Team from it.

module Football
{
    root Team
    {
        string name { unique; }
        detail players Player.team;
        date founded;
        specification searchByName 't => t.name == name'
        {
            string name;
        }
        history;
    }
    root Player
    {
        string name;
        string? nickname;
        Team? *team;
        int number;
        index (nickname) where 'p => p.nickname != null';
        specification searchByNickname 'p => p.nickname == search'
        {
            string search;
        }
        history;
    }
    report TeamReport
    {
        string teamName;
        Team team 't => t.name == teamName';
        templater createPdf 'TeamReport.docx' pdf;
    }
}

Using template TeamReport.docx our PHP code returns pdf object to user for download.

public function createReportForTeam($team)
{
    $report = new TeamReport();
    $report->teamName = $team;
    return $report->createPdf();
}

header('Content-Type: application/pdf');
header('Content-Disposition: attachment; filename=TeamReport.pdf');
echo createReportForTeam('New School');

If we decide to create report with multiple teams, we'll just provide array of team names and create report based on found Teams. Our new report would look like:

report TeamReport
{
    string[] names;
    Team[] teams 't => names.Contains(t.name)';
    templater createPdf 'TeamReportAll.docx' pdf;
}

We can even reuse existing TeamReport.docx with some minor changes and we are ready to download pdf. Using only three lines of DSL code we implemented creating team report for our football team. Report concept is useful in few other, not so obvious, ways. Most effort in creating good web applications is in minimizing number of requests sent from client to server. We can use report concept to define multiple data sources aggregated in single object. This way we can minimize number of request for different data sources.

This tutorial shows some non-trivial applications of few concepts. This is still only scratching the surface of what's available and possible with DSL platform. In next tutorial we will show some very advanced features exposed through relatively simple concepts.