PHP Developers Network

A community of PHP developers offering assistance, advice, discussion, and friendship.
 
Loading
It is currently Tue Aug 22, 2017 12:21 am

All times are UTC - 5 hours




Post new topic Reply to topic  [ 1 post ] 
Author Message
PostPosted: Sun Aug 16, 2015 5:48 pm 
Offline
Moderator
User avatar

Joined: Tue Nov 09, 2010 3:39 pm
Posts: 6268
Location: Montreal, Canada
Lesson 0 - Setup
Lesson 1 - Database abstraction
Lesson 2.5 - Cleanup
Lesson 3 - Exceptions and logging

With our database issues at least somewhat addressed, let's move on to routing. Getting proper routing set up will help decouple our URIs from our PHP scripts, will allow greater flexibility in changing our routes, and will help set us on the path of separation of concerns. The purist in me wants to reach for the Symfony components -- and I strongly encourage you to give those a look -- but for the sake of simplicity, let's take a look at Klein. I have used this router in a number of small projects and love its small footprint and lack of dependencies.

Step 1, of course, is to have Composer pull down this new dependency. At the time of writing, the current stable version is 2.1.0, so we'll update our composer.json to look like this

Syntax: [ Download ] [ Hide ]
{
    "require": {
        "vlucas/phpdotenv": "~2.0",
        "klein/klein": "~2.1"
    }
}
 

Now we run composer update again and we're ready to start using Klein.

There are a few things we're going to be doing here, and we'll be doing them all at once because they're all interrelated. In order to properly make use of our new router, we're going to move away from page based scripts (ie. foo.php) and start using what's known as the Front Controller Pattern. At its simplest, that's a fancy way of saying we send all requests to index.php, and let the code in there sort things out. Why is this desirable? For one, having all of our requests flow through a given location means we can handle a lot of application bootstrapping in a single location. This means not having to call session_start or include our dbconn.php in every single file. This means looking at just our route definitions will give us a good high level idea of what's going on across the entirety of the application. This also means we're going to be doing a little restructuring. Rather than having individual page scripts each pull in header, footer, etc, we'll move to a single layout we'll load and into which we'll inject the view specific to the request we're currently responding to. It's really a very similar idea to what we're seeing in the application now, just approached from a different perspective. Also, with every request going through our front controller, we can move the other views out of the document root and into our app/ directory.

Let's start by creating a new directory to hold our layout and our individual views. We could create this as a sibling of app or as a subdirectory within it. Doesn't really matter. As Squeaker is a fairly small application and isn't going to be a ton of files, let's just create it inside app/. We'll create a templates directory inside app, and then a layouts and a views directory inside that. Our directory structure, then, will look like this

Syntax: [ Download ] [ Hide ]
app/
    templates/
        layouts/
        views/
public/
vendor/


Now if we're going to be using index.php as our starting point, we're going to need move the contents of our existing index.php some place else. So here's what we'll do. Since most of the markup in index.php is the same as what's in header.php and footer.php, let's use it as the base for our main layout. Let's copy the entirety of public/index.php to app/templates/layouts/main.php. We can now get rid of everything in public/index.php and get started defining some routes. Before we can create a new Klein object, we need to call Composer's autoloader so our application knows where to find it. But didn't we already do that in dbconn.php? And isn't repetition bad? We did, and it is. To keep things simple for now, let's copy the code from dbconn.php into our public/index.php and just get rid of dbconn.php altogether. We won't be needing that anymore. Our public/index.php should now look like this

Syntax: [ Download ] [ Hide ]
<?php

error_reporting(E_ALL);

require_once dirname(__DIR__) . '/vendor/autoload.php';
require_once dirname(__DIR__) . '/app/DB.php';

$config = new Dotenv\Dotenv(dirname(__DIR__));
$config->load();

$pdo = new PDO('mysql:host=' . getenv('DBHOST') . ';dbname=' . getenv('DBNAME'), getenv('DBUSER'), getenv('DBPASS'));
$db = new Squeaker\DB($pdo);


Now we can finally instantiate the router and get started.

Syntax: [ Download ] [ Hide ]
$router = new Klein\Klein;

$router->get('/', function() {
    echo "Hello, World";
});

$router->dispatch();
 

If we now load up Squeaker in our browser, we get greeted with Hello, World. Small start, to be sure, but we'll build back up. Let's quickly go over what's happening here.

Syntax: [ Download ] [ Hide ]
$router = new Klein\Klein;

No magic here, just creating an instance of Klein.

Syntax: [ Download ] [ Hide ]
$router->get('/', function() {
    echo "Hello, World";
});


Now that we have an instance of the router, we want to start defining some routes. The home page is going to be a GET request -- most pages will, in fact -- so we use the get method and pass it first the URL we want it to match -- / for the root of the app -- and then a callback to execute when that route is matched.

Syntax: [ Download ] [ Hide ]
$router->dispatch();

With our routes defined, we tell the router to respond to them. That's really all there is to it. Let's add another.

Syntax: [ Download ] [ Hide ]
$router->get('/another', function() {
    echo "This is another page.";
});


Add that before the dispatch call, point our browser to /another and... oh. That was unexpected. We stated that we wanted all our requests to go through the front controller but never took steps to make sure that happened. Let's create a simple .htaccess redirect in our document root to make sure all requests actually go to index.php

Syntax: [ Download ] [ Hide ]
RewriteEngine On
RewriteCond %{REQUEST_FILENAME} !-d
RewriteCond %{REQUEST_FILENAME} !-f
RewriteRule . /index.php [L]
 

Now if we reload we get the expected behaviour. Great, but these aren't the actual pages we want to be loading. Let's start with the home page. The callback we're passing to get actually accepts four parameters, request, response, service, and app. We can then tell the service to render a view we specify.

Syntax: [ Download ] [ Hide ]
$router->get('/', function($request, $response, $service, $app) {
    $service->render(dirname(__DIR__) . '/app/templates/layouts/main.php');
});


(Don't forget to remove the dbconn requirement from the layout as we have deleted that file)

Loading the main page in the browser again, we're back to seeing what we're used to. We're making progress. Still, the layouts directory is meant to contain layouts that affect all pages and the individual page elements are meant to be in the views directory. In order to achieve this, we need to identify which parts of the layout are common to all pages and extract the remaining portion into what will be its view. Comparing this layout against our public/header.php, it's clear that everything up to the opening div.body tag is part of the layout, and we'll want to close all of the tags we've opened, leaving just the jumbotron as our landing page view. Let's go ahead and cut that bit out and paste it into a new file; app/templates/views/index.php. We replace the code we just removed with a call to yieldView so the correct view gets injected, and all that's left is making a few changes to our route definition.

Our route definition now looks like this

Syntax: [ Download ] [ Hide ]
$router->get('/', function($request, $response, $service, $app) {
    $service->layout(dirname(__DIR__) . '/app/templates/layouts/main.php');
    $service->render(dirname(__DIR__) . '/app/templates/views/index.php');
});


our layout like this

Syntax: [ Download ] [ Hide ]
<?php

session_start();

?>

<!DOCTYPE html>
<html lang="en">
    <head>
        <meta charset="utf-8">
        <meta http-equiv="X-UA-Compatible" content="IE=edge">
        <meta name="viewport" content="width=device-width, initial-scale=1">
        <title>Squeaker</title>
        <link rel="stylesheet" href="//maxcdn.bootstrapcdn.com/bootstrap/3.3.5/css/bootstrap.min.css">
        <link rel="stylesheet" href="style.css">
    </head>
    <body>
        <nav class="navbar navbar-default navbar-fixed-top">
            <div class="container">
                <div class="navbar-header">
                    <button type="button" class="navbar-toggle collapsed" data-toggle="collapse" data-target="#navbar" aria-expanded="false" aria-controls="navbar">
                        <span class="sr-only">Toggle navigation</span>
                        <span class="icon-bar"></span>
                        <span class="icon-bar"></span>
                        <span class="icon-bar"></span>
                    </button>
                    <?php if (isset($_SESSION['username'])): ?>
                        <a class="navbar-brand" href="home.php">Squeaker</a>
                    <?php else: ?>
                        <a class="navbar-brand" href="/">Squeaker</a>
                    <?php endif; ?>
                </div>
                <div id="navbar" class="navbar-collapse collapse">
                    <ul class="nav navbar-nav">
                        <?php if (isset($_SESSION['username'])): ?>
                            <li><a href="me.php">Me</a></li>
                            <li><a href="profile.php">Profile</a></li>
                        <?php endif; ?>
                        <li><a href="browse.php">Browse</a></li>
                    </ul>
                    <ul class="nav navbar-nav pull-right">
                        <?php if (isset($_SESSION['username'])): ?>
                            <li><a href="logout.php">Logout</a></li>
                        <?php else: ?>
                            <li><a href="login.php">Login</a></li>
                        <?php endif; ?>
                    </ul>
                </div>
            </div>
        </nav>
        <div class="container body">
            <div class="row">
                <?= $this->yieldView(); ?>
            </div>
        </div>
        <script src="//code.jquery.com/jquery-1.11.3.min.js"></script>
        <script src="//maxcdn.bootstrapcdn.com/bootstrap/3.3.5/js/bootstrap.min.js"></script>
    </body>
</html>


and our view like this

Syntax: [ Download ] [ Hide ]
<div class="jumbotron">
    <h1>Welcome to Squeaker!</h1>
    <p>This totally isn't a badly done Twitter clone, I promise. It has cupcakes and stuff.</p>
    <div class="clearfix">
        <a href="register.php" class="btn btn-lg btn-primary pull-right">Register</a>
    </div>
</div>


Pretty good. Let's set up our browse route next. We'll start by moving our public/browse.php to app/templates/views/browse.php and set up our route like so

Syntax: [ Download ] [ Hide ]
$router->get('/browse', function($request, $response, $service, $app) {
    $service->layout(dirname(__DIR__) . '/app/templates/layouts/main.php');
    $service->render(dirname(__DIR__) . '/app/templates/views/browse.php');
});


Because we'll be incorporating this into a layout, we need to make a couple of changes to the file. We need to get rid of the include calls to header and footer as they're no longer needed, we need to remove the require call to dbconn as it no longer exists, and we've already started a session in our layout, so we can remove session_start as well. With that all trimmed out, our view looks something like this

Syntax: [ Download ] [ Hide ]
<?php

if (isset($_SESSION['username'])) {
    $user_id   = $db->getUserID($_SESSION['username']);
    $following = $db->getUsersBeingFollowed($user_id);
    $users     = $db->getUsersExcept($user_id);
} else {
    $following = array();
    $users     = $db->getAllUsers();
}

?>

<?php if (!empty($users)): ?>
    <ul class="users">
        <?php foreach ($users as $user): ?>
            <li>
                <article>
                    <h4>
                        <a href="user.php?user=<?= $user->id; ?>">
                            <?= !empty($user->display_name) ? $user->display_name : $user->username; ?>
                        </a>
                    </h4>
                    <div class="following pull-right">
                        <?php if (in_array($user->id, $following)): ?>
                            <a href="unfollow.php?user=<?= $user->id; ?>" class="btn btn-danger">Unfollow</a>
                        <?php else: ?>
                            <a href="follow.php?user=<?= $user->id; ?>" class="btn btn-info">Follow</a>
                        <?php endif; ?>
                    </div>
                </article>
            </li>
        <?php endforeach; ?>
    </ul>
<?php endif; ?>
 

And if we load up /browse in our browser... we get errors. $db not defined? Hmm. We clearly defined it in our bootstrapping code. Two things to consider here: 1. It's not defined within the scope of our route callback, and 2. DB calls don't really belong in the view anyway. What we want to do is make the DB object available to our routes, perform whatever calls we need to within our route callbacks, and then pass the relevant results to the view for display. This latter is an important point when we're talking about the separation of concerns. Every part of the application has a responsibility. Views are responsible for displaying our markup and nothing else. They should be devoid of all but the simplest logic; simple conditionals and iterating over results sets. With that in mind, let's move that logic out of the view and into our route definition.

Syntax: [ Download ] [ Hide ]
$router->get('/browse', function($request, $response, $service, $app) use ($db) {
    if (isset($_SESSION['username'])) {
        $user_id   = $db->getUserID($_SESSION['username']);
        $following = $db->getUsersBeingFollowed($user_id);
        $users     = $db->getUsersExcept($user_id);
    } else {
        $following = array();
        $users     = $db->getAllUsers();
    }

    $service->layout(dirname(__DIR__) . '/app/templates/layouts/main.php');
    $service->render(dirname(__DIR__) . '/app/templates/views/browse.php', ['users' => $users, 'following' => $following]);
});


Note that on the render line, we're passing in the variables we want available in the view as an array of key/value pairs. The array key will be the properties through which the values are available. We're setting them as properties of Klein's service provider, so we must access them in the view as $this->users rather than just $users.

Syntax: [ Download ] [ Hide ]
<?php if (!empty($this->users)): ?>
    <ul class="users">
        <?php foreach ($this->users as $user): ?>
            <li>
                <article>
                    <h4>
                        <a href="user.php?user=<?= $user->id; ?>">
                            <?= !empty($user->display_name) ? $user->display_name : $user->username; ?>
                        </a>
                    </h4>
                    <div class="following pull-right">
                        <?php if (in_array($user->id, $this->following)): ?>
                            <a href="unfollow.php?user=<?= $user->id; ?>" class="btn btn-danger">Unfollow</a>
                        <?php else: ?>
                            <a href="follow.php?user=<?= $user->id; ?>" class="btn btn-info">Follow</a>
                        <?php endif; ?>
                    </div>
                </article>
            </li>
        <?php endforeach; ?>
    </ul>
<?php endif; ?>


We now have our view displaying everything it was before, but we have removed all domain logic from the view itself. It is now solely a presentation element. Of course, things didn't go perfectly. Our main column wasn't previously this wide. We could fix that in the view, but that would mean having to fix it in every view, and we're trying to move away from duplication of code. Though it's not obvious now because we aren't logged in, we're also missing the sidebar content. We've got a single layout file and three possible layouts:

  • no sidebar, full width content (as in the landing page)
  • no sidebar, narrow content (edit profile page, browse when not logged in)
  • sidebar, narrow content

To simplify things a little, we can remove the distinction between no sidebar and an empty sidebar when the content column is narrow, so now we need only worry about whether our content is full width or not. Let's change our single yieldView line to the following

Syntax: [ Download ] [ Hide ]
<?php if ($this->sidebar): ?>
    <div class="col-md-3">
        <?php // Sidebar will go here ?>
    </div>
    <div class="col-md-6">
        <?= $this->yieldView(); ?>
    </div>
<?php else: ?>
    <?= $this->yieldView(); ?>
<?php endif; ?>


Now we can pass $sidebar to our view and page should render as expected.

Most of our remaining routes require us to be logged in, so let's tackle the login page next.

So we know we need to move the login.php into our views directory, strip out the calls to session_start, dbconn, header and footer, but what about this $_POST business? Where does that belong? We have two options here; we can specify a single route to handle both rendering the view and handling form submissions, or we can separate it out into two separate routes, one for GET requests and one for POST requests. Either is fine, but I generally prefer the latter. We're looking at something like this

Syntax: [ Download ] [ Hide ]
$router->get('/login', function($request, $response, $service, $app) {
    $service->sidebar = true;
    $service->layout(dirname(__DIR__) . '/app/templates/layouts/main.php');
    $service->render(dirname(__DIR__) . '/app/templates/views/login.php');
});

$router->post('/login', function($request, $response, $service, $app) use ($db) {
    if (!empty($_POST)) {
        $logged = $db->login($_POST['username'], $_POST['password']);
        if ($logged === true) {
            $_SESSION['username'] = $_POST['username'];
            header('Location: home.php');
            exit;
        } else {
            $error = "Incorrect username or password";
        }
    }
});


Our login form also uses $_SERVER['PHP_SELF'] for its action, which will no longer work as we're using a single point of entry and $_SERVER['PHP_SELF'] will always be index.php. What we want to do is point it to the appropriate resource. In this case, we're posting back to the same URI, so we can just call the current URI with $this->request->uri().

The functionality technically works, but the redirect doesn't. We can clean up the code a little by moving away from superglobals ($_POST, $_SESSION) and by leveraging Klein's redirect functionality instead of calling header directly. We don't need the
Syntax: [ Download ] [ Hide ]
if (!empty($_POST))
anymore as we know this callback is only being executed when a POST request is sent to /login. With a little refactoring, we've got something like this

Syntax: [ Download ] [ Hide ]
$router->post('/login', function($request, $response, $service, $app) use ($db) {
    $logged = $db->login($request->param('username'), $request->param('password'));
    if ($logged === true) {
        $_SESSION['username'] = $request->param('username');
        $response->redirect('/home');
    } else {
        $error = "Incorrect username or password";
    }
});

$router->get('/home', function($request, $response, $service, $app) use ($db) {
    echo 'Home'; exit;
});
 

We grab the username and password from the request object rather than the $_POST superglobal, pass them to our login function, and redirect to a temporary route. Everything works great. What if our credentials are wrong, though? We'll need to redirect the user back and display the error message. How do we pass that parameter through to our GET route? Here we're going to leverage flash messages. Simply put, these are messages that get stored in a user's session and are deleted immediately after having been displayed. More goodness that already comes into this lightweight but surprisingly feature-rich router.

So we set our error message as a flash message, have our GET route check for such messages, and modify our view to display them

Syntax: [ Download ] [ Hide ]
$router->get('/login', function($request, $response, $service, $app) {
    $error = $service->flashes('error');

    $service->sidebar = true;
    $service->layout(dirname(__DIR__) . '/app/templates/layouts/main.php');
    $service->render(dirname(__DIR__) . '/app/templates/views/login.php', ['error' => $error]);
});

$router->post('/login', function($request, $response, $service, $app) use ($db) {
    $logged = $db->login($request->param('username'), $request->param('password'));
    if ($logged === true) {
        $_SESSION['username'] = $request->param('username');
        $response->redirect('/home');
    } else {
        $service->flash('Incorrect username or password', 'error');
        $service->back();
    }
});
 

Syntax: [ Download ] [ Hide ]
<?php if (isset($this->error)): ?>
    <?php foreach ($this->error as $error): ?>
    <p class="alert alert-danger"><?= $error; ?></p>
    <?php endforeach; ?>
<?php endif; ?>

<form action="<?= $this->request->uri(); ?>" method="post">
    <div class="form-group">
        <label for="username">Username</label>
        <input type="text" name="username" class="form-control">
    </div>
    <div class="form-group">
        <label for="password">Password</label>
        <input type="password" name="password" class="form-control">
    </div>
    <button class="btn btn-lg btn-block btn-primary">Login</button>
</form>


This all appears to be working properly, but now we're getting complaints of a session already having been started. What's going on there? Digging through Klein's code pertaining to flash messages, we can see that it checks for the existence of a session ID in its service provider and, if none is found, attempts to create one. The conflict is coming from our session_start call in our layout. Seeing as this isn't a display element, it's not really the responsibility of the view layer, so let's remove that.

The error is gone, our flash message is displaying as intended, and we're being redirected to our temporary home route on successful login. Let's work on fleshing out that route next.

As we've done a few times already, let's move home.php out of our public webroot and into our views directory, strip out the session, dbconn, header, and footer calls, and start moving our domain logic into our route callback. Our trimmed down view now looks like this

Syntax: [ Download ] [ Hide ]
<form action="<?= $this->request->uri(); ?>" method="post">
    <div class="form-group">
        <textarea name="squeak" class="form-control" rows="5"></textarea>
    </div>
    <div class="form-group">
        <button class="btn btn-primary pull-right">Squeak!</button>
    </div>
</form>

<div class="clearfix"></div>

<?php if (!empty($this->squeaks)): ?>
    <ul class="squeaks">
        <?php foreach ($this->squeaks as $squeak): ?>
            <li class="squeak-container">
                <article class="squeak-body">
                    <a href="user.php?user=<?= $squeak->user_id; ?>">
                        <?= !empty($squeak->display_name) ? $squeak->display_name : $squeak->username; ?>
                    </a> &middot;
                    <time><?= $squeak->created_at; ?></time>
                    <div class="squeak"><?= $squeak->message; ?></div>
                </article>
            </li>
        <?php endforeach; ?>
    </ul>
<?php endif; ?>


and our route like this

Syntax: [ Download ] [ Hide ]
$router->get('/home', function($request, $response, $service, $app) use ($db) {
    if (!isset($_SESSION['username'])) {
        header('Location: login.php');
    } else {
        $user_id = $db->getUserID($_SESSION['username']);
    }

    if (!empty($_POST['squeak'])) {
        $db->createSqueak($user_id, $_POST['squeak']);
    }
    $squeaks = $db->getSqueaksForHome($user_id);

    $service->sidebar = true;
    $service->layout(dirname(__DIR__) . '/app/templates/layouts/main.php');
    $service->render(dirname(__DIR__) . '/app/templates/views/home.php', ['squeaks' => $squeaks]);
});


So what's wrong here? What do we need to address? We've already discussed getting rid of calls to header, and our $_POST logic should probably go in its own route definition. Beyond that, though, we're starting to see some repetition creeping its way into our routes. We're defining $sidebar in every route, we're specifying our layout in every route, and these are really things that are consistent throughout the app. We should be able to define them once and be done with them. We can accomplish this be defining a respond call without specifying a URI. As the router will execute every callback for route definitions that match the current request URI, this callback will get executed on every single request, which is really what we're after here.

Syntax: [ Download ] [ Hide ]
$router->respond(function($request, $response, $service, $app) use ($db) {
    $service->startSession();

    $service->layout_dir = dirname(__DIR__) . '/app/templates/layouts/';
    $service->views_dir  = dirname(__DIR__) . '/app/templates/views/';

    $service->sidebar = true;
    $service->layout($service->layout_dir . 'main.php');
});


Now our session is started, our layout is defined, default sidebar value is set, and we've assigned our layout and views directories to properties to reduce the amount of typing we have to do. We're starting to get some more meaningful abstraction happening, and we're further decreasing repeated code. We're still missing our sidebar, though. For the time being at least, let's wrap all the function calls that make up the sidebar logic into a single function, just for the sake of simplicity.

Syntax: [ Download ] [ Hide ]
public function getUserSidebar($user_id) {
    $profile = $this->getProfile($user_id);
    $squeaks = $this->getSqueakCount($user_id);
    $following = $this->getFollowingCount($user_id);
    $followers = $this->getFollowerCount($user_id);

    return [
        'user_id' => $user_id,
        'profile' => $profile,
        'squeaks' => $squeaks,
        'following' => $following,
        'followers' => $followers,
    ];
}


We can now call that from our catch-all route and override it later if necessary, namely for the individual user pages.

Syntax: [ Download ] [ Hide ]
if (isset($_SESSION['username'])) {
    $user_id = $db->getUserID($_SESSION['username']);
    $service->sidebar_data = $db->getUserSidebar($user_id);
}


We update our sidebar code to reference the newly created array, and we have a functioning sidebar again.

Syntax: [ Download ] [ Hide ]
<?php if (isset($this->sidebar_data)): ?>
<article class="userinfo">
    <h3>
        <?php if (isset($this->sidebar_data['profile']->display_name) && !empty($this->sidebar_data['profile']->display_name)): ?>
            <?= "{$this->sidebar_data['profile']->display_name} ({$this->sidebar_data['profile']->username})"; ?>
        <?php else: ?>
            <?= $this->sidebar_data['profile']->username; ?>
        <?php endif; ?>
    </h3>

    <?php if (!empty($this->sidebar_data['profile']->profile)): ?>
        <div class="profile"><?= $this->sidebar_data['profile']->profile; ?></div>
    <?php endif; ?>

    <?php if (!empty($this->sidebar_data['profile']->website)): ?>
        <div class="website"><a href="<?= $this->sidebar_data['profile']->website; ?>" target="_blank"><?= $this->sidebar_data['profile']->website; ?></a></div>
    <?php endif; ?>

    <div class="row stats">
        <div class="col-md-4 text-center">
            <a href="me.php">
                <strong>Squeaks</strong>
                <?= $this->sidebar_data['squeaks']; ?>
            </a>
        </div>
        <div class="col-md-4 text-center">
            <a href="following.php">
                <strong>Following</strong>
                <?= $this->sidebar_data['following']; ?>
            </a>
        </div>
        <div class="col-md-4 text-center">
            <a href="followers.php">
                <strong>Followers</strong>
                <?= $this->sidebar_data['followers']; ?>
            </a>
        </div>
    </div>
</article>
<?php endif; ?>


Now that we have a recognizable process established, I'm going to go through the remaining pages commenting only when we encounter something new or unusual. If you're following along, you'll want to do the same and, as usual, the finished product will be available both as a tag in our Git repo and as a download at the end of this lesson.

So the first 'something new' comes when converting the registration page. I'm using Klein's built in validator to check the form submission. When a rule fails, an exception is thrown, we catch that, and use the exception's message to return an error to the user. I have also defined a couple of new validation rules as the existing set doesn't entirely meet our needs.

Syntax: [ Download ] [ Hide ]
$router->post('/register', function($request, $response, $service, $app) use ($db) {
    $service->addValidator('usernameAvailable', function ($value) use ($db) {
        return $db->usernameAvailable($value);
    });
    $service->addValidator('matches', function($first, $second) {
        return $first === $second;
    });

    try {
        $service->validateParam('username', 'Username is required')->notNull();
        $service->validateParam('password', 'Password is required')->notNull();
        $service->validateParam('password_confirmation', 'Password confirmation is required')->notNull();
        $service->validateParam('password_confirmation', 'Passwords do not match')->matches($request->param('password'));
        $service->validateParam('username', 'That username is already taken')->usernameAvailable();

        $created = $db->register($request->param('username'), $request->param('password'));
        if ($created) {
            $response->redirect('login');
        }
    } catch (Exception $e) {
        $service->flash($e->getMessage(), 'error');
        $service->back();
    }
});


One other 'something new' is route parameters as seen in a number of routes. Previously, we'd have user.php?user=123. We have replaced that with a nicer /user/123

Syntax: [ Download ] [ Hide ]
$router->get('/user/[i:id]', function($request, $response, $service, $app) use ($db) {
    $squeaks = $db->getSqueaksForUser($request->param('id'));
    $service->sidebar_data = $db->getUserSidebar($request->param('id'));
    $service->render($service->views_dir . 'user.php', ['squeaks' => $squeaks]);
});
 

Where [i:id] means we're capturing an integer and calling it id. This is available within the callback as a request parameter which we can pass to any functions we're calling within that route callback.

All the routes have now been converted to be handled by our router. Most of the old page scripts have been trimmed down and moved to our views directory. Some, namely those that didn't display anything, have been done away with altogether. The route callback handles the functionality, there was no display, and we just redirect somewhere else once we're done. This leaves our public directory nice and clean and our app directory logically structured. There are still a few improvements we could make here, but we have already covered a lot of ground, so I think this is a good place to wrap up this lesson. We can tackle the clean up next time before diving into error handling.

Download lesson

_________________
Supported PHP versions No longer supported versions


Top
 Profile  
 
Display posts from previous:  Sort by  
Post new topic Reply to topic  [ 1 post ] 

All times are UTC - 5 hours


Who is online

Users browsing this forum: No registered users and 2 guests


You cannot post new topics in this forum
You cannot reply to topics in this forum
You cannot edit your posts in this forum
You cannot delete your posts in this forum
You cannot post attachments in this forum

Jump to:  
Powered by phpBB® Forum Software © phpBB Group