Hello {you} with Slim Framework 3.x

Last week I wrote my Slim 3.x "Hello World" tutorial after hearing Josh Lockhart introduce the framework at our UpstatePHP meet-up. I was eager to learn the basics of using Slim as an HTTP router, and since then I've continued to learn new things (yay!), like using named routes / arguments and its dependency injection container (based on SensioLabs's oddly named Pimple).

Let's break it down here...

Named Routes

Defining a route is pretty straightforward. Slim's App class gives us methods to add routes named after their correlating HTTP methods (e.g. get(), post(), etc.). To give a route a name, you simply append a call to setName() to its definition:

<?php
$app
->get('/hello', function(Slim\Http\Request $request, Slim\Http\Response $response, array $args) {
  return
$response->write('Hello, world!');
})->
setName('hello-world');
?>

Why would you want to do this? There may be times in your application where you want to redirect from one route to another or create a link to another route. Instead of hardcoding route paths into your code, use Router::pathFor() to let Slim generate the path for you:

<?php
$app
->get('/some/other/route', function($request, $response, $args) {
 
$path = $this->getContainer()->get('router')->pathFor('hello-world');
 
// ...
});
?>

In Slim 2.x and prior, this was named urlFor(). Yours truly played semantic stickler to propose renaming it in a pull request that was recently merged by Rob Allen, a.k.a @akrabat, who offers a great intro to Slim 3.x in his Slim 3 Primer and numerous blog posts.

Dynamic Routes with Named Arguments

In addition to static routes like the ones defined above, Slim depends on FastRoute to support dynamic routes that use named arguments in their path patterns. Curly braces identify named arguments in a route's pattern, and regular expressions further determine what request paths match a route:

<?php
// Add a route for a personal greeting using an argument.
$app->get('/hello/{name:[A-Za-z]+}', function(Slim\Http\Request $request, Slim\Http\Response $response, array $args) {
  return
$response->write('Hello, ' . $args['name'] . '!');
})->
setName('hello-name');
?>

To create a path for this route, I would use the same function from above but pass it an array including the named argument:

<?php
// From inside a route closure...
$path = $this->getContainer()->get('router')->pathFor('hello-name', ['name' => 'Shady']);
?>

The regular expression in the pattern will only permit a match to this route if the request path includes a second argument containing only upper or lower case letters. Throw a number, a space, or punctuation in there, and you'll get a 404. Granted, if I were actually writing an application, I would use a template engine like Twig to automatically escape variables when generating output.

Slim's Dependency Injection Container

Ahh, what a fine segue that was!

To use Twig in a Slim application, you might make use of Slim's dependency injection container (or DIC). I'm still relatively new to the concept, so I won't try to explain the design pattern in depth.

At a high level, the DIC is an object that your application uses to find other objects that encapsulate discrete bits of functionality. Pimple refers to these objects as services, and they may be responsible for communicating with the database, sending mail, or rendering output through a template engine.

When you create a new Slim App object, you pass in the container object like so:

<?php
// Prepare the Pimple dependency injection container.
$container = new \Slim\Container();

// Add a Twig service to the container.
$container['twig'] = function($container) {
 
$loader = new Twig_Loader_Filesystem('templates');
  return new
Twig_Environment($loader, array('cache'));
};

// Create the Slim application using our container.
$app = new \Slim\App($container);
?>

Now any route can make use of the Twig service to build output via a template:

<?php
$app
->get('/hello', function(Slim\Http\Request $request, Slim\Http\Response $response) {
 
// Load the template through the Twig service in the DIC.
 
$template = $this->getContainer()->get('twig')->loadTemplate('index.html');
 
// Render the template using a simple content variable.
 
return $response->write($template->render(['content' => 'Hello, world!']));
})->
setName('hello-world');
?>

The Slim DIC also contains an array of settings that you can append values to when you construct it by passing in an associative array of settings:

<?php
// Prepare the Pimple dependency injection container.
$container = new \Slim\Container([
 
'site_name' => 'Slim Shady',
]);
?>

These settings are appended to a default settings array and may be used elsewhere in the code. Slim binds route closures (the function we defined in above examples as the second argument to the $app->get() calls) to the $app object, so a quick reference to the site_name setting would look like:

<?php
$app
->get('/hello/{name:[A-Za-z]+}', function(Slim\Http\Request $request, Slim\Http\Response $response, array $args) {
 
// Load the template through the Twig service in the DIC.
 
$template = $this->getContainer()->get('twig')->loadTemplate('index.html');
 
// Render the template using content and site name variables.
 
return $response->write($template->render([
   
'content' => 'Hello, ' . $args['name'] . '!',
   
'site_name' => $this->settings['site_name'],
  ]));
})->
setName('hello-name');
?>

You can learn more about the DIC in the Slim 3.x dependency injection documentation or Rob Allen's comprehensive blog post on accessing services in Slim 3.x.

A final word...

I'm used to using the Devel module for debugging when writing Drupal modules. I was a bit lost writing PHP outside of Drupal at first, but then I remembered that Devel for Drupal 8 uses the Kint library to provide fantastic variable / backtrace debugging support.

To use it in your Slim application, add it to your composer.json file and the autoloader will allow you to make automatic use of it. The composer.json for my little learning application is currently:

{
    "require": {
        "slim/slim": "dev-develop",
        "raveren/kint": "^1.0",
        "twig/twig": "~1.0"
    }
}

Adding a simple line like the following to a route closure will produce some easily navigable debug output to the page:

<?php
d
($this->settings);
?>

Seriously... it's magic. There's never been a better time to lose yourself in the PHP Renaissance.

Hello World with Slim Framework 3.x

We were fortunate to have Josh Lockhart (a.k.a. @codeguy) join us at tonight's UpstatePHP meet-up. He is the creator of Slim Framework and author of Modern PHP (should be in your library) / PHP the Right Way (should be in your bookmarks), which basically makes him a leader of the PHP renaissance.

I loved PHP the Right Way but hadn't heard of Slim yet, so it was great to hear him present the upcoming 3.x version this evening.

Slim currently describes itself as "a PHP micro framework that helps you quickly write simple yet powerful web applications and APIs." Slim provides you with an intelligent router to handle incoming HTTP requests. It implements HTTP request and response objects according to PSR-7 and manages a simple first-in-first-out stack for each route (made up of "middleware" callbacks and the route's own callback) that it invokes in turn to generate a response.

I'm easily distracted by new things, so I couldn't wait to get home and put together a quick "Hello, world!" application using Slim 3.x.

To get started, I created a quick local project directory (yep, slimjim), changed into it, and grabbed the framework and its dependencies using Composer:

composer require slim/slim:dev-develop

I then created a small index.php file that includes Composer's autoload file, instantiates a new Slim application object, and registered a simple GET request route. Routes require two parameters, a URI pattern (that may include argument tokens) and a PHP callable that accepts request, response, and argument parameters.

Here I've used a simple closure that echoes the greeting:

<?php
require 'vendor/autoload.php';

$app = new \Slim\App();

$app->get('/', function($request, $response, $args) {
    echo
'Hello, world!';
});

$app->run();
?>

To test it out, I fired up PHP's built in web server:

php -S localhost:8000

And then navigated to http://localhost:8000 to see my handsome message.

I made a backwards compatibility inspired gaffe on purpose in my first test by echoing the output directly. In Slim 3.x, your callback is really supposed to write output through the response object and return it instead. It still supports the echo method from previous versions through some ob_start() trickery, but you really should stop doing that and write better code like so:

<?php
require 'vendor/autoload.php';

$app = new \Slim\App();

$app->get('/', function(Slim\Http\Request $request, Slim\Http\Response $response, array $args) {
   
$response->write('Hello, world!');
    return
$response;
});

$app->run();
?>

Note that I'm also a recent phpStorm adoptee, so type hinting my closure's parameters makes for a much nicer development experience. Time to step up!

I look forward to playing with Slim some more.

Why not combine shopping carts on user login?

When I first wrote Ubercart's Cart module, we knew we were going to support both anonymous and authenticated shopping carts and checkout. The decision came at a time when there wasn't consensus around the impact of forced login on conversions, but we knew we wanted it to be optional if at all possible. Additionally, for authenticated users, we wanted to preserve items in their shopping carts so they would see the same items when logging in from multiple devices or across multiple sessions.

This resulted in a small conflict that we had to figure out how to deal with: users could have items in their authenticated shopping carts but browse the site anonymously, create a new shopping cart, and then log in. What should happen to the items in their authenticated carts vs. the items in their anonymous carts?

There are three basic resolutions: combine the shopping carts together so the user still has a single shopping cart, remove the items from the previous session and leave it up to the customer to find them again if desired, or retain the old shopping cart but ignore it until the customer has completed checkout for the current cart. In Ubercart, I chose to combine the items, but in Drupal Commerce I changed course to retain the old cart but, from the customer's point of view, treat that anonymously created cart as the current cart after login.

We got some push back for this decision, but ultimately I didn't change the default functionality of Drupal Commerce. We just made sure there was an appropriate hook (hook_commerce_cart_order_convert()) so developers could alter this behavior on a site-by-site basis as need be.

From the merchant's standpoint, the thinking behind combining carts goes that you don't want customers to forget they intended to purchase those products in the past. However, from the customer's standpoint, suddenly having additional items in the cart after logging in during the checkout process is quite jarring.

In fact, I've been bitten by this behavior when shopping online at Barnes & Noble. Weeks prior to placing an order, I had put a Wheel of Time novel in my shopping cart but eventually bought the book in store. When I came back to the site to purchase a gift for my wife, I used a login button on the checkout form to quickly reuse my previous addresses and payment details. Unbeknownst to me, the website combined my old shopping cart with my current one such that my "quick checkout" experience made me accidentally order a book I already owned! I then had to spend 30 minutes with customer service canceling the order and placing it afresh just for the book I actually wanted.

That experience confirmed in my mind we made the correct decision not to combine carts automatically. As eCommerce framework developers, we have no clue where a developer might like to integrate login during the checkout process. Best to let them decide if it's safe to do something with those previous cart items instead of silently making the decision for them.

That said, I believe we can improve the experience even further. Right now, Drupal Commerce retains the old shopping cart order, and after the customer completes checkout they'll see the previous shopping cart as their current cart. This can be confusing as well!

My ideal situation would likely be a user interface component on the shopping cart page where customers can see items they had added to their carts in previous sessions, giving them the option to add those products to their current carts. If they decide not to, I don't see any harm in then just deleting those historical carts and moving on.

There's always room for improvement. Smile

Photo credit: alphageek

Come to DrupalCon Latin America 2015

I've been privileged to attend almost every DrupalCon since Barcelona in 2007. I missed Paris in 2009, but I had a good excuse - my wife was due to give birth to our first child around the same time.

The relocation of the Commerce Guys headquarters to Paris has given me plenty of time to catch up on the missed sightseeing, but I still need to figure out how to get to Sydney after missing that one. Lol

Without access to those hundreds of Drupal developers and enthusiasts in 2007, I never would have known anyone was even using Ubercart. I didn't know how to engage other developers remotely (my early forays into IRC were similar to webchick's, I believe), and there wasn't much going on in Louisville, KY where I called home. Meeting others in the Drupal community, learning from my peers, and being mentored directly by many of the same has grown me personally and professionally in ways I never would have expected.

That's why I'm excited about the opportunity to travel to Bogotá, Colombia for the first DrupalCon in Latin America, February 10-12. I can't wait to hear the keynotes from both Dries and Larry, two of my Drupal heroes, and to learn more about the latest developments in Drupal 8 core and contributed modules.

I'll personally be addressing two topics: Drupal Commerce 2.x for Drupal 8 (on behalf of bojanz) and growing a Drupal based product business. I also look forward to the conversations, shared meals, and sprints that make the conference so rewarding.

I strongly encourage you to come if you're in a position to do so! Smile

With the help of Carlos Ospina, I've recorded a personal invitation in Spanish that I trust doesn't have me saying anything embarrassing. I'm sure my Spanish will be better for at least a week after spending time at the conference. Tongue

Pages