Skip to main content
Code Tutorial

WordPress MVC with TypeRocket for Admin Tutorial

In this tutorial, we will be making an example project that allows administrators to book flights for customers.

Before you begin, install TypeRocket and be sure to configure the Galaxy CLI. Thoughout this tutorial we will be using the TypeRocket starter theme.

Desired Features

Here is a list of the features we want to use:

  • Database: ORM + Migrations
  • Design Patterns: MVC
  • Forms Custom Fields
  • Security
  • Dev Tools: Debugging + CLI

Here is what the client wants:

  1. Flights: Can't be a post type
  2. Seats: Will have many Flights
  3. JSON API: Needs to be public and paginate
  4. Admin: Needs to be restricted

Custom MVC Backends

First, create a database migration using the Galaxy CLI.

Database Migration

php galaxy make:migration add_flights_table
-- Description:
-- >>> Up >>>
-- >>> Down >>>

Next, Add the SQL needed to create the database tables.

-- Description: Flights Table
-- >>> Up >>>
CREATE TABLE `{
Pro Only Feature
prefix
Pro Only Feature
}flights` ( `id` int(11) unsigned NOT NULL AUTO_INCREMENT, `customer_id` int(11) DEFAULT NULL, `seat_id` int(11) DEFAULT NULL, `notes` longtext, `meta` json DEFAULT NULL, PRIMARY KEY (`id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; -- >>> Down >>> DROP TABLE IF EXISTS `{
Pro Only Feature
prefix
Pro Only Feature
}flights`

Run the migration.

php galaxy migrate up

Model: Flight

Now, create a model with a controller for the database table. The -c will also generate the controller class for us.

php galaxy make:model -c base Flight

For example, with the model in place, you can access your database records using the TypeRocket ORM.

(new Flight)->find(1); 
(new Flight)->where('seat_id', 1)->get();
(new Flight)->customer()->get();

Next, map the customer_id column to the WordPress users table using the App/User model using a model relationship.

class Flight extends Model
{
    protected $resource = 'flights';

    public function customer()
    {
        return $this->belongsTo(User::class, 'customer_id');
    }
}

Then, connect users to their flights.

class User extends WPUser
{
    public function flights()
    {
        return $this->hasMany(Flight::class, 'customer_id');
    }
}

Setting up these relationships opens the door to quickly querying models by relation.

(new Flight)->customer()->get();
(new User)->flights()->get();

Post Type: Seat

For the seat data, and mainly for example, use a custom post type.

In your theme's functions.php file add the following.

tr_post_type('Seat')
    ->setIcon('dashicons-tickets-alt')
    ->featureless()
    ->setAdminOnly()
    ->addColumn('Row', true)
    ->addColumn('Letter')
    ->setMainForm(function() {
        $form = tr_form();
        echo $form->select('Row')->searchable()->setOptions(range(1, 15), 'flat');
        echo $form->radio('Letter')->setOptions(['A','C','D','F'], 'flat');
    })->saveTitleAs(function (\App\Models\Seat $seat) {
        return $seat->meta->row.$seat->meta->letter;
    });

Model: Seat

Next, create a model for the post type.

php galaxy make:model -c post Seat

Setup the relationship with flights.

class Seat extends WPPost
{
    protected $postType = 'seat';

    public function flights()
    {
        return $this->hasMany(Flight::class, 'seat_id');
    }
}
class Flight extends Model
{
    // ...

    public function seat()
    {
        return $this->belongsTo(Seat::class, 'seat_id');
    }
}

Next, apply the model to the post type.

tr_post_type('Seat')
    ->setModelClass(/App/Models/Seat::class) // new line
    ->setIcon('dashicons-tickets-alt')
    ->featureless()
    ->setAdminOnly()
    ->addColumn('Row', true)
    ->addColumn('Letter')
    ->setMainForm(function() {
        $form = tr_form();
        echo $form->select('Row')->searchable()->setOptions(range(1, 15), 'flat');
        echo $form->radio('Letter')->setOptions(['A','C','D','F'], 'flat');
    })->saveTitleAs(function (\App\Models\Seat $seat) {
        return $seat->meta->row.$seat->meta->letter;
    });

Admin Pages: Flight

With the models in place, we can start making a UI that will enable end-users to interact with the database.

fit

First, register a custom MVC backend resource admin UI pages. This will set up the pages needed for the controllers.

tr_resource_pages('Flight')->setIcon('dashicons-airplane');

Controller: Flight Index

To work with flights, we will need to edit the FlightController class under app/Controllers/FlightController.php. The controller will return an index veiw.

<?php
class FlightController extends Controller
{
    public function index()
    {
        return tr_view('flights.index');
    }
}

Views: Flight Index Page

For the index view the file resources/views/index.php.

<?php
$table = tr_table();
$table->setColumns([
    'customer.display_name' => [
        'label' => 'Customer',
        'actions' => ['edit', 'view', 'delete']
    ],
    'seat.post_title' => [
        'label' => 'Seat',
    ],
    'id' => [
        'sort' => true,
        'label' => 'ID'
    ]
], 'customer.display_name');
$table->render();

Views: Flight Add & Edit Pages

Next, create views for the add and edit pages alike.

public function add()
{
    $form = tr_form('flight');
    $button = 'Add';

    return tr_view('flights.form', compact('form', 'button'));
}

public function edit(Flight $flight)
{
    $form = tr_form($flight);
    $button = 'Update';

    return tr_view('flights.form', compact('form', 'button'));
}

The view file for these can be the same resources/views/form.php.

<?php
/** @var \App\Elements\Form $form */
echo $form->save($button)->setFields(

    $form->select('Seat')->setName('seat_id')
        ->setModelOptions(\App\Models\Seat::class),

    $form->select('Customer')->setName('customer_id')
        ->setModelOptions(\App\Models\User::class),

    $form->editor('Notes'),

    $form->repeater('Meta')->setFields(
        $form->row($form->text('Key'), $form->text('Value'))
    )->hideContract()

);

Model: Flight Record Formatting

When data is saved to the database via the view's form, you will want to format the data before it is saved. You can do this with the $format property. When the data is fetched from the database, you will want to $cast the data to a format that suits your needs.

In the flight example, format the data to JSON before saving and then cast it to an object when pulling it back out.

class Flight extends Model
{
    // ...

    protected $format = [
        'meta' => 'json_encode'
    ];

    protected $cast = [
        'meta' => 'object'
    ];
}

Model: Flight Security

Next, declare the fields that are safe to save using the ORM with the $fillable property.

class Flight extends Model
{
    // ...

    protected $fillable = [
        'seat_id',
        'customer_id',
        'notes',
        'meta',
    ];

}

In the admin UI, with debug mode on, you will see the fillable icon next to each field.

Controller: Flight Create & Update

With the views and models in place, its safe to start saving data to the database via the controller.

public function create(Request $request)
{
    $flight = new Flight;
    $flight->save($request->fields());

    return tr_redirect()
        ->toPage('flight', 'index')
        ->withFlash('Flight created!');
}

public function update(Flight $flight, Request $request)
{
    $flight->save($request->fields());

    return tr_redirect()
        ->toPage('flight', 'edit', $flight->getID())
        ->withFlash('Flight updated!');
}

Controller: Flight Destroy

Next, add the destroy method for the controller. And make sure the current user is allowed to perform the action.

public function destroy(Flight $flight, Response $response)
{
    if(!$flight->can('delete')) {
        $response->unauthorized('Unauthorized: Flight not deleted')->abort();
    }

    $flight->delete();

    return $response->warning('Flight deleted!');
}

Policies

Policies give you control over how the line $flight->can('delete') works. Create a policy using the Galaxy CLI.

php galaxy make:policy FlightPolicy

Next, define who can delete a record by restricting access to only users with the capability manage_flights.

class FlightPolicy extends Policy
{
    public function delete(AuthUser $auth, $object)
    {
        if( $auth->isCapable('manage_flights') ) {
            return true;
        }

        return false;
    }
}

Role: Custom

WordPress does not yet know the users that can manage_flights. Add access to this capability to all administrator users.

To add capabilities, WordPress likes to add them via plugins. You should also take this approach. You can name the plugin file anything you like.

<?php
/*
Plugin Name: Flight TypeRocket Roles
Version:  1.0
Description:  For managing roles.
Author:  TypeRocket
License: GPLv2 or later
*/

register_activation_hook( __FILE__, function() {
		add_action('typerocket_loaded', function() {
				tr_roles()->updateRolesCapabilities('administrator', ['manage_flights']);
		});
});

Now, activate your plugin.

Auth Service

Next, you need to register your new FlightPolicy to the auth service.

class AuthService extends Authorizer
{
    protected $policies = [
        // Models
        '\App\Models\Flight' => '\App\Auth\FlightPolicy',
        '\App\Models\Post' => '\App\Auth\PostPolicy',
        '\App\Models\Page' => '\App\Auth\PagePolicy',
        // ...
    ];
}

Now, go back to the admin and try deleting a flight with an admin. Your users should be authorized for the following action.

if(!$flight->can('delete')) {
    $response->unauthorized('Unauthorized: Flight not deleted')->abort();
}

JSON API

If you wanted to make a JSON API for the custom data you can do it using TypeRocket routes by adding one to the routes/public.php.

tr_route()->get()->on('flights/customer/*', function(User $user) {
    return $user->flights()->paginate(2);
});

Now, when you visit the URL https://example.com/'flights/customer/1 you will see your JSON API, as long as you have the record.

{
  "items": [
    {
      "id": "1",
      "customer_id": "1",
      "seat_id": "9",
      "notes": null,
      "meta": {
        "15835551205210": {
          "key": "kevin",
          "value": "dees"
        }
      }
    }
  ],
  "current": "2",
  "pages": 2,
  "count": "3",
  "links": {
    "current": null,
    "next": null,
    "previous": "http://localhost:8888/flights/customer/1/?paged=1",
    "first": "http://localhost:8888/flights/customer/1/?paged=1",
    "last": "http://localhost:8888/flights/customer/1/?paged=2"
  }
}

Next Steps

Next, lets look at saving our custom post type fields to a custom table instead of post meta.

Access More TypeRocket

Join our community mailing list and get notified before anyone else. Get videos and more.

By using our website you agree to our Cookie policy.