Skip to content

Latest commit

 

History

History
534 lines (392 loc) · 17 KB

getting_started.md

File metadata and controls

534 lines (392 loc) · 17 KB

Getting started: setup a project

So you want to use or learn Minz? In this chapter, I’ll show you how to setup your first project. I prefer to warn you: there are a lot to do manually, but in the end, Minz should be very helpful.

In this guide, I’ll assume that you use Linux, that you understand basic command line instructions and that you’ve already setup your Web development environment with PHP 8.1+. Are we good?

In this chapter, we’ll simply learn how to display (and test!) “Hello World!” when opening the home page.

A word about Minz

You might ask: “Why should I use a framework?” If you don’t have the answer, I recommend you to NOT use Minz. In general, I recommend to not complexify your setup if you don’t need it.

I designed Minz to be used for small to medium applications that need a minimal architecture. Its value lies in its simplicity. You can have a look at the source code, there are very few files and I think they are easy to read and hack. Please note I’ll often invite you to explore the source code. If you don’t explore the code of Minz, you probably use it the wrong way.

In the rest of this chapter, I’ll show you the basic operation of Minz: transforming the browser requests into Minz Response via a controller action. In the end, you’ll also learn how to test controller actions to be sure they always do what you want.

The classes featured in this chapter are: \Minz\Request, \Minz\Response, \Minz\Router, \Minz\Configuration.

Serving the index file

First, create a new folder for this project with a basic home page:

$ mkdir /var/www/hello/
$ cd /var/www/hello/
$ # from now on, I'll always assume that you current directory is /var/www/hello!
$ mkdir public/
$ echo '<?php echo "Hello World!";' > public/index.php

Make sure your webserver is configured to serve the public/ folder as the root folder. For instance, my NGINX server is configured as follow:

server {
    listen       80;
    listen       [::]:80;
    server_name  localhost;
    root         /var/www/hello/public;

    index index.php index.html;

    location / {
        try_files $uri $uri/ /index.php$is_args$query_string;
    }

    location ~ index.php$ {
        fastcgi_pass unix:/run/php-fpm/www.sock;

        fastcgi_split_path_info ^(.+\.php)(/.*)$;
        fastcgi_param PATH_INFO $fastcgi_path_info;
    }
}

You can adapt to your own needs, but keep in mind the location sections are important to get right. The idea is to try to serve existing files under the public/ folder, and if there’s no matching file, to serve the URL with the index.php file.

Don’t forget to reload your webserver, e.g. with systemd and Nginx:

$ sudo systemctl reload nginx

If your configuration is correct, you should see Hello World! welcoming you when you open a Web browser at localhost/test. Common mistakes involve a bad webserver configuration, or wrong file permissions (i.e. the www-data or nginx user not being able to access the files).

Once you have this right, we’ll setup Minz.

Installing Minz

First, create some more folders:

$ mkdir bin/
$ mkdir configuration/
$ mkdir lib/
$ mkdir src/
$ mkdir tests/

We’ll download Minz under the lib/ folder:

$ wget https://github.com/flusio/Minz/archive/refs/heads/main.zip
$ unzip main.zip
$ rm main.zip
$ mv Minz-main/ lib/Minz

Alternatively, you can git clone the Minz repository under the lib/ folder. That's what I always do, but I recommend that you clone your own fork so you’ll be able to hack Minz the way that you want later.

Now, let’s create an autoloader for the Minz classes as well as your app classes. Create a new autoload.php file at the root of your project:

<?php

spl_autoload_register(
    function ($class_name) {
        $app_namespace = 'App';
        $app_path = __DIR__;
        $lib_path = $app_path . '/lib';

        if (str_starts_with($class_name, 'Minz')) {
            include $lib_path . '/Minz/autoload.php';
        } elseif (str_starts_with($class_name, $app_namespace)) {
            $class_name = substr($class_name, strlen($app_namespace) + 1);
            $class_path = str_replace('\\', '/', $class_name) . '.php';
            include $app_path . '/src/' . $class_path;
        }
    }
);

Finally, you can load Minz in your public/index.php file:

<?php

$app_path = realpath(__DIR__ . '/..');

include $app_path . '/autoload.php';

\Minz\Configuration::load('development', $app_path);

echo 'Hello World!';

If you open localhost, you should see an error (or a blank page). It tells you Minz wasn’t able to load the configuration file. Let’s create configuration/environment_development.php:

<?php

return [
    'secret_key' => 'a secret key',

    'url_options' => [
        'host' => 'localhost',
    ],
];

This is the most basic configuration file that you can write. Now, you should see the “Hello World!” again. The Configuration class allows you to configure a lot of things like the database and mailer information, some paths, or even your application configuration.

To learn more, explore the \Minz\Configuration class.

Introducing Requests

Minz provides a class to abstract the HTTP requests: \Minz\Request. It contains the parameters (e.g. GET and POST parameters), as well as the headers. You have to initialize the request only once in your index file, then it will be passed to the different Minz components. Abstracting the requests makes it a lot easier to write tests.

For now, I’ll just show you how to use it to change the word “World” by a parameter passed via a GET parameter. Add the following lines after the environment initialization:

<?php

// ...

$http_method = strtoupper($_SERVER['REQUEST_METHOD']); // e.g. 'GET' or 'POST'
$http_uri = $_SERVER['REQUEST_URI']; // e.g. '/test'
$http_parameters = array_merge($_GET, $_POST);
$http_headers = $_SERVER;

$request = new \Minz\Request($http_method, $http_uri, $http_parameters, $http_headers);

// The second argument is the default value to return. If it wasn't given, the
// method would return `null` when the `name` parameter is missing.
$name = $request->param('name', 'World');
echo "Hello {$name}!";

You can notice the difference by opening localhost and localhost/?name=Charlie.

Now, you might think it’s tedious and prone to errors to initialize the Request manually. That’s why there’s a method for that: initFromGlobals(). Internally, it does essentially what we saw above.

<?php

// ...

$request = \Minz\Request::initFromGlobals();

$name = $request->param('name', 'World');
echo "Hello {$name}!";

To learn more, explore the \Minz\Request class.

Introducing Responses

Minz abstracts the HTTP responses the same way as the requests but with a different class: \Minz\Response. To build a Response, you have to set a HTTP status (e.g. 200, 404), some content to display and optional additional HTTP headers.

This class is generally instantiated by a controller action and used by the index file. For now, we’ll instantiate it in the index file as well. We’ll tell it to return a text response:

<?php

// ...

$request = \Minz\Request::initFromGlobals();

$name = $request->param('name', 'World');

$response = \Minz\Response::text(200, "Hello {$name}!");

// we declare the HTTP headers to the browser
http_response_code($response->code());
foreach ($response->headers() as $header) {
    header($header);
}

// and we display the page content
echo $response->render();

If you open the website again, you’ll notice the text is rendered differently. This is because we’re now returning text and not HTML (i.e. Minz generates a Content-Type: text/plain header). In fact, the text() method is usually used with command line actions. We’ll fix that in a future chapter.

As for Requests, it’s possible to shorten the code to send the Response to the client:

<?php

// ...

$request = \Minz\Request::initFromGlobals();

$name = $request->param('name', 'World');

$response = \Minz\Response::text(200, "Hello {$name}!");

\Minz\Response::sendByHttp($response);

To learn more, explore the \Minz\Response class.

Introducing routing and controllers

We’ve seen the two most important classes of Minz: Request and Response. These classes allow us to test Minz applications easily. I’ll explain you how to test in the next section, but for now we’ll move the code that we want to test in a dedicated controller action.

A controller is a class dedicated to a specific part of your application, usually a ressource (e.g. users or posts). The controllers methods are called “actions”. They are dedicated to a specific activity such as listing users, creation or deletion. An action always takes a \Minz\Request in parameter and returns a \Minz\Response. Finally, an action is associated to a couple of a HTTP verb (e.g. GET, POST) and an URL (e.g. /posts).

The previous paragraph contains a lot of important information. I recommend you to take the time to understand each sentence. If it’s still too abstract for you, don’t worry: the rest of this section will show you how all these things are working.

First, let’s create a controller with a single action. Create a new file named src/Home.php.

<?php

namespace App;

// The Home class is our controller
class Home
{
    // and show() is an action of this controller, it takes a $request
    public function show($request)
    {
        // the code of the action is simply what we wrote earlier in the index
        // page
        $name = $request->param('name', 'World');
        $response = \Minz\Response::text(200, "Hello {$name}!");

        // and it returns a $response
        return $response;
    }
}

We just wrote our first controller action. The last thing to do is to associate it to a couple of HTTP verb and URL: it’s time to introduce the routing mechanism of Minz! Routing is handled by the \Minz\Router class. Where should you declare the routes? It’s mainly up to you: you could do it in the index file for instance. However, Minz comes with a convention to make it easier to write tests: let’s write a new src/Application.php file.

<?php

namespace App;

class Application
{
    public function run($request)
    {
        // This is where we’ll declare our routes
        $router = new \Minz\Router();

        // We create a new route to connect the URL `GET /` to our action.
        // The syntax is `controller#action`.
        $router->addRoute('GET', '/', 'Home#show');

        \Minz\Engine::init($router);

        return \Minz\Engine::run($request);
    }
}

The Application constructor should take no required parameter and should declare a run() method taking a $request as unique parameter and returning a $response. This is pure convention, you can choose to architecture your application differently, but keep in mind it allows you to test your controllers more easily.

We’re almost done: all we have to do is to use the Application class in our index file:

// ...

$request = \Minz\Request::initFromGlobals();

$application = new \App\Application();
$response = $application->run($request);

\Minz\Response::sendByHttp($response);

You can test that the application still work by opening again localhost and localhost/?name=Charlie. More important, if you open localhost/test, you should get an error telling you that “Path "GET /test" doesn’t match any route.” It’s because we didn’t create the corresponding route in the router. The error is ugly for now, but we’ll fix that later.

To learn more, explore the \Minz\Router class.

Testing controllers

We’re almost at the end of this chapter! In this last section, I want to teach you how to test controllers actions. You could probably develop with Minz without learning that, but I strongly believe that tests should not be considered as secondary work; this is why I’ll introduce you to these in this first chapter.

Tests will be written with PHPUnit. Let’s install it first under the bin/ folder:

$ curl -LO https://phar.phpunit.de/phpunit-9.5.phar
$ mv phpunit-9.5.phar bin/phpunit
$ chmod u+x bin/phpunit

You should also consider to verify the signature of the PHPUnit file, but as it can be tedious, I consider it's out of the scope of this documentation.

Let’s write a tests/bootstrap.php file, it will be loaded by PHPUnit:

<?php

$app_path = realpath(__DIR__ . '/..');

include $app_path . '/autoload.php';
\Minz\Configuration::load('test', $app_path);

Note how similar it is compared to the beginning of the index file. You might have guessed we now need a new configuration file. We can simply copy the existing one:

$ cp configuration/environment_development.php configuration/environment_test.php

It’s now time to write our first tests in tests/HomeTest.php:

<?php

namespace App;

class HomeTest extends \PHPUnit\Framework\TestCase
{
    use \Minz\Tests\ApplicationHelper;
    use \Minz\Tests\ResponseAsserts;

    public function testShowReturnsOkStatusCode()
    {
        $response = $this->appRun('GET', '/');

        $this->assertResponseCode($response, 200);
    }

    public function testShowReturnsContentTypeText()
    {
        $response = $this->appRun('GET', '/');

        $this->assertResponseHeaders($response, [
            'Content-Type' => 'text/plain',
        ]);
    }

    public function testShowRendersHelloWorld()
    {
        $response = $this->appRun('GET', '/');

        $this->assertResponseEquals($response, 'Hello World!');
    }

    public function testShowRendersGivenParameterName()
    {
        $name = 'Charlie';

        $response = $this->appRun('GET', '/', [
            'name' => $name,
        ]);

        $this->assertResponseEquals($response, "Hello {$name}!");
    }
}

To execute the tests and check that everything goes fine, run the following command:

$ ./bin/phpunit --bootstrap ./tests/bootstrap.php --testdox ./tests/

The tests are pretty standard for PHPUnit tests except this appRun method and custom assertions. You may have noticed these two lines:

use \Minz\Tests\ApplicationHelper;
use \Minz\Tests\ResponseAsserts;

This is where all the magic happens: they add methods to the HomeTest class in order to facilitate our tests.

Remember when I told you that the Application class was just a convention? It was to allow the appRun method provided by the ApplicationHelper class to work. It does a bit of magic by loading the application from \App\Application, creating a \Minz\Request from the parameters that you give to it, and executing it against the run method of Application. Finally it returns the \Minz\Response returned by the controller action. The beauty of this method is that you can test almost all the stack of your application very easily (including the routing).

To learn more, explore the \Minz\Tests\ApplicationHelper class.

The ResponseAsserts, for its part, provides a bunch of assertions to test the returned response. I personnaly use them a lot.

To learn more, explore the \Minz\Tests\ResponseAsserts class.

A note about magic: I usually don’t like magic in the code too much (“explicit is better than implicit”). However, I sometimes like to add a pinch of magic in order to enhance the experience of developers. I always do my best to make it optional so you are free to adopt whatever philosophy you prefer. In this case, it would totally be possible to instantiate the Request and Application manually. However, I prefer my tests to contain the bare minimum information to make them easier to be read.

Conclusion

In this chapter, we’ve seen the most important components of Minz. I recommend you to be sure you understood every bit of it before going any further.

To summarise, Minz abstracts HTTP requests with the \Minz\Request class, and HTTP responses with the \Minz\Response class. It transfers a request to a controller action via the routing mechanism (i.e. \Minz\Router). The action returns a response which is then outputed by the index file. Finally, Minz provides a bunch of useful methods to help you to test your controllers easily.

Next steps to learn Minz: