This is part 1 of the series “Simple Membership PHP Tutorial”. This series is going to consist of 4 parts:

  1. Simple MVC. [this post]
  2. Implementing user login and registration system. [coming soon]
  3. Recurring payments with PayPal. [coming soon]
  4. Recurring payments with Stripe. [coming soon]

Part 1 – Simple MVC

While creating a membership system, we want to ensure that we create a secure application which our end users can use safely, and also work faster and use less resources possible.

The MVC pattern is the most efficient and simple to work with while building a site. I have not used an MVC framework for this project, but implemented a quick and simple one, with the help of FastRoute, a fast request router for PHP, and some 200-400 lines of code.

We can begin with a simple introduction to this project and its components, and how to install it in the end of this article.

URL Rewriting

We want to prevent apache or nginx from throwing 404/403 errors while it reads the request URL and looks for the appropriate files or directories, we want to instead rewrite all request to our main application’s index.php file. Doing that, the app can then receive the full request URI and parses it to determine the responsible controller for this request, based on the route and the request method.

Apache2:

Rewriting URLs to index.php for apache2 can be done by simply adding a .htaccess file in the root of our project with the following code:

<IfModule mod_rewrite.c>
RewriteEngine On

RewriteBase /simple-membership/

# enforce no trailing slashes
RewriteCond %{REQUEST_FILENAME} !-d
RewriteRule ^(.*)/$ /simple-membership/$1 [L,R=301]

# rewrite
RewriteRule ^index\.php$ - [L]
RewriteCond %{REQUEST_FILENAME} !-f
RewriteCond %{REQUEST_FILENAME} !-d
RewriteRule . /simple-membership/index.php [L]
</IfModule>

If you are going to install this on the root of your domain or subdomain, then make sure to replace all of the /simple-membership/ occurrences with a slash instead /,  otherwise, make sure to place the correct directory name as well instead of simple-membership.

Nginx:

This is coming up. I haven’t tested this on an nginx server yet but will do very soon. Will update to add the server block code.

URL Routing

After rewriting the URLs, now if we visit whatever URL it will serve the index.php file, and of course a 404 page because we don’t have a controller defined for that route yet.

Here’s a tree view of the project structure:

App/
├── Config.php
├── Ctrl
│   ├── Ctrl.php
│   ├── Err.php
│   └── Home.php
├── Routes.php
├── view
│   ├── home.php
│   ├── index.html
│   └── template.php
└── View.php

The models live in App/*.php files, the views or template files are placed inside App/views/ directory, while the controllers can be added to App/Ctrl/ directory in as PHP files, and they should be classes extending the main controller class App\Ctrl\Ctrl.php.

Adding a new controller

It’s very easy to do so, taking advantage of the existing tools. You want to specify the route to target, for example say we want to respond to requests accessing http://example.com/login: we create a new file Login.php or whatever the name as long as with a PHP extension, and add the following code:

<?php

namespace App\Ctrl;

class Login extends Ctrl
{
    static $route = '/login';
}

The Login class (our controller) extends the Ctrl class, and specifies the route in a static public class variable named $route, this variable is accessed by the routes collector while registering the application routes.

Next up, you want to add a method to actually respond to the requests. You can make use of GET and POST methods:

public function get()
{

}

public function post()
{

}

And then the get will serve GET requests, and post for the POST requests (for form submissions, do actions and then redirect the page so the request becomes GET).

Or, if you want to respond to all request methods, then you can declare a request method:

public function request()
{

}

Now so far we have this code:

<?php

namespace App\Ctrl;

class Login extends Ctrl
{
    static $route = '/login';
    static $pageTitle = 'Login';

    public function get()
    {
        // respond to HTTP GET
    }

    public function post()
    {
        // respond to HTTP POST
    }
}

If we visit /simple-membership/login, it will show a blank page because we did not return any view yet within our get method. To do so, we’re going to use the View model which is defined in App/View.php file.

Responding with a view

App\View model class allows us to either make a view out of some string:

public function get()
{
    return View::make('Hello World!');
}

which should show a “Hello World!” string wrapped in our template container, or load the view from a file and pass variables to this file:

public function get()
{
    // respond to HTTP GET
    return View::file('hello', [
        'name' => 'World',
    ]);
}

so we create a view file at App/view/hello.php with

Hello, <?php echo $name; ?>!

as the content. Visiting /simple-membership/login again will display the same content. We can pass variables to our view by adding them to an array as the 2nd argument for View::file method. The keys will be used as variable names.

Hello world simple membership

Hello world simple membership

The Login::post can be used to listen for POST requests, in case the user submits our form, and then we will process the form and redirect to the same URI (refreshing the page) with some response which we can read from get method. Why don’t we use just Login::request for both requests? I know, we want to prevent the user to resubmit the form again by refreshing/reloading the page, so we can make sure we do our own things only once. This can be explained further in part 2 of this tutorial, as we’ll get to dive into the actual Login controller class and others.

While redirecting POST to GET, you can add UI messages such as errors, success messages and warnings using App\Errors::addError method, a static instance of the errors can be retrieved using self::getErrors() method. Also, you can keep the form data and pass them to get method using data key of the redirect arguments.

The POST method callback:

public function post()
{
    return self::redirectHere(array(
        'data' => [ 'foo' => $_POST['bar'] ],
        'errors' => self::getErrors()->addError('This is some error message', 'error'),
    ));
}

The GET method callback:

public function get()
{
    return View::file('hello', [
        'name' => 'World',
        'err' => self::getErrors(),
    ]);
}

The view file becomes:

<div class="tiny-box">
    <span class="user-err"><?php print_errors($err); ?></span>
    <h1>Hello, <?php echo $name; ?>!</h1>

    <form method="post">
        <button>Test Submit</button>
    </form>
</div>

Hello word simple membership ui errors

The form data and also errors are temporarily store over light cookies, and fetched and deleted while on the next request. To get a submitted value then you can use old() function, which you should pass the name of the form field to it as the first argument:

<input type="text" name="username" value="<?php echo esc_attr( old('username') ); ?>" />

The esc_attr() function (and many others) are literally copied from WordPress framework to help us prevent XSS. You can find other global functions inside the helpers.php file in the root, where you can define your custom functions as well if needed to.

Other Useful Ctrl Methods

There are many methods that your controller file should have since it extends App\Ctrl\Ctrl class, here are some:

Login::head(): this one basically gives you the opportunity to add stylesheets and other meta tags to the head part of our HTML. By default, it parses our main stylesheet:

static function head()
{
    echo '<link rel="stylesheet" type="text/css" href="', View::url('/assets/css/style.css', true), '">', PHP_EOL;
}

But you can alter that, or add call parent::head() and parse more custom HTML for this controller.

Login::footer(): it’s the same as the previous one, except this one could be use to parse our JavaScript or other HTML elements in the foot of our template.

static function footer()
{
    echo '<script>alert("It\'s working!")</script>', PHP_EOL;
}

Login::url(): returns the full or relative URL to this route which a controller is responsible for, examples:

echo Login::url(); // http://localhost.dev/simple-membership/login
echo Profile::url('?foo=bar'); // http://localhost.dev/simple-membership/profile?foo=bar
echo Register::url('#form', true); // /simple-membership/register#form

Login::canonical(): This one returns the full URL to the current route, without the query string or the hashes or whatever characters are added to the URL.

Login::redirectHere(): You can use this to redirect the request to a given controller route, you can keep the UI messages and the form data and migrate them from the current route you’re redirecting from to the target route. Examples:

return Home::redirectHere([
  'errors' => self::getErrors(),
  'data' => [
    'foo' => 'bar',
  ],
]);

so in our example, if any UI messages are there, they’ll be shown in the target redirect-to controller view, and old('foo') should return bar from there as well.

Installing Simple Membership

You can install this project which is currently hosted on Github: elhardoum/simple-membership:

1. Clone the project to your local environment:

git clone https://github.com/elhardoum/simple-membership && cd simple-membership

2. Install the dependencies with composer:

composer install

3. Create a database for the project and the database tables (2 tables for the moment):

mysql -u root -p < db.sql

Setup Simple Membership

Make sure to open App/Config.php and configure your app correctly, there are many configurations such as the MySQL credentials, and the mail settings. More will be added as I continue working on this project.

I will continue this series with writing part 2 of this tutorial some day next week.

Digital Ocean

Cheap Cloud SSD Hosting

Get a VPS now starting at $5/m, fast and perfect for WordPress and PHP applications

Sign Up with $10 Credit