Building Web App

Welcome to the guide on how to build web app using PHPy 2 framework.

Setup PHP & Nginx

It's recommended to use PHP & Nginx for serving apps based on PHPy. Install latest stable versions:

sudo apt install php-fpm nginx

Creating PHPy app

Clone PHPy:

git clone git clone https://github.com/mrcrypster/phpy.git

Create empty folder for the new app:

mkdir /var/www/my-app

Init new app:

php phpy/phpy.php /var/www/my-app

This will generate app structure and show Nginx config to use for the app:

server {
  root /var/www/my-app/web;
  index index.php;

  server_name myapp;
  location / {
    try_files $uri /index.php?$args /index.php?$args;
  }

  location ~ \.php$ {
    include snippets/fastcgi-php.conf;
    fastcgi_pass unix:/run/php/php-fpm.sock;
  }
}

Make sure fastcgi_pass points to your fpm socket and add to Nginx (reload configs with nginx -s reload).

Point browser to http://localhost (or other host configured, like localhost:88 in our case) and check if you see the following:

Layout & default action

You'll find following files under my-app folder:

web/             # images, styles and other public resources goes here
   /index.php    # entry point to process all app requests

app/             # our app is implemented here 
   /layout.php   # global HTML layout (actions are rendered inside)
   /default.php  # default action (for "/" requests) to process and render

Layout

Global app/layout.php file is used to render common app elements and resources (that appear on all pages). Let's edit it:

<?php return ['html' => [ ':v' => 1, # this is css/js files cache versions ':title' => 'PHPy2 App', # HTML page title 'h1#logo' => 'My app', # <h1 id="logo">My app</h1> 'div.content' => phpy() # <div class="content">...</div> ]];

As you can see, there's a phpy() function call. What is does, is executes and renders nested action based on request URI.

Default action

When we call root / path of our app, default.php action is being called and rendered (inside layout.php). We'll edit it a little:

<?php return [ 'h2' => 'I am default page', 'p' => 'It is ' . date('H:i') . ' now', ];

Now let's check how it all looks in the browser:

Creating & nesting actions

Request URIs are automatically mapped to actions. E.g. in order to process /test URI, we should create app/test.php action:

<?php return [ 'p' => $_SERVER['REQUEST_URI'] # which will print current URI ];

Now let's look what http://localhost/test shows:

We can put actions into folders. E.g. creating app/docs/article.php will handle http://localhost/docs/article requests. Infinite level of folding is allowed.

We can also create default.php file under folders, which will be executed when URI is the same as folder path. E.g. let's create app/my/default.php action:

<?php return [ 'p' => 'You are under "/my" folder' ];

Navigate our browser to http://localhost/my to see the following:

Nesting

We can nest actions to keep files small, which is highly recommended. E.g. let's add app/my/menu.php action:

<?php return [ 'ul' => [ ['li' => ['a:/my' => 'My home']], ['li' => ['a:/' => 'Public home']] ] ];

In order to nest this action into my/default.php action we need to use phpy() function with an action we want to nest:

<?php return [ 'div.menu' => phpy('/my/menu'), # menu will be rendered inside # <div class="menu"></div> block 'p' => 'You are under "/my" folder' ];

Navigate our browser to http://localhost/my to see how nesting works:

We can pass variables to nested actions as a second phpy() argument:

<?php return [ 'div.menu' => phpy('/my/menu', ['page' => 'default']), # Now $page variable will be accessible in menu.php ];

Infinite level of nesting is allowed.

Client-side actions & handlers

PHPy comes with Javascript layer to manipulate actions on the client side. Button elements are recommended to be used to trigger actions. Let's add a button to default.php action:

<?php return [ 'h2' => 'I am default page', 'p#time' => 'It is ' . date('H:i') . ' now', 'button:/time' => 'Update time' ];

Now /time action will be called once button is clicked:

Let's create time.php action to render something in return once button is clicked:

<?php return [ '#time' => date('H:i:s') ];

PHPy will automatically find DOM element with id=time and place action rendered output to it:

Any number of DOM elements can be updated from single action:

<?php return [ '#time' => date('H:i:s'), '#other' => 'Some other element content', ... ];

Custom Javascript handlers

We can call custom Javascript functions as well. Let's add one more button that calls hi() function:

<?php return [ '...', 'button:hi()' => 'Say hi' ];

The Javascript hi() function itself can be defined anywhere, but it's recommended to place relevant JS implementations under separate JS files for each action. That's why we create app/default.js with the following code:

function hi() { alert("Hi!"); }

By default, all JS files are automatically collected and loaded to app, so we don't have to do anything else before checking browser:

Forms

Forms are handled by PHPy in the same manner as buttons. We just declare form and specify endpoint action to process submitted data. Let's create a form in /my/default.php action

<?php return [ 'div.menu' => phpy('/my/menu'), 'form:/my/submit' => [ 'input:email' => '', 'submit' => 'Go', '#result' => '' ] ];

This form will be submitted to /my/submit action. We have also created empty #result element which we'll use to render response from action. Let's declare /my/submit.php action:

<?php $email = $_POST['email']; return [ '#result' => 'You have submitted ' . $email . ' email' ];

After we fill form field and click "Go" button, we can see result rendered by server:

CSS

CSS is also automatically collected and loaded. It is recommended to put action-related styles into separate files and have global styles in layout.css file:

app/
   /layout.css  # General CSS styles
   ...
   /action.php  # PHP action implementation
   /action.js   # Javascript client-side handlers/listeners/...
   /action.css  # CSS styles for this action

Let's add some styling to our lauout.css file:

body { padding: 1em; } h1 { border-bottom: 1px solid #eee; padding: 0 0 .5em 0; } ul { padding: 0; margin: 0 0 2em 0; } ul li { display: inline-block; padding: 0; margin: 0 1em 0 0; } form { background: #eee; text-align: center; padding: 2em; } input { margin: 0 1em 0 0; }

Now everything feels a bit better:

Custom routing

If we need to process specific endpoint in a highly custom way, we can do that using phpy::on() in web/index.php file:

phpy::on('/test', function() { echo 'Custom response!!!'; });

This callback function will be executed once http://localhost/test URI is called:

curl http://localhost/test
Custom response!!!

If callback returns true then standard flow will be executed after function is finished:

phpy::on('/admin', function() { if ( i_am_admin() ) { return true; } else { die('Access denied'); } });

We can use regular expressions as well as stack multiple handlers:

phpy::on('/some.*/', function() { ... }); # all '/some*' endpoints phpy::on('/some', function() { ... }); # single '/some' endpoint

Sessions & authentication

Session are not started automatically, so we need to add session_start() to web/index.php if you plan you use sessions:

<?php session_start(); # insert at the top of the file ...

Now standard PHP session becomes accessible in any part of the app:

$_SESSION['pages'] += 1;

Authentication

User specific areas access control can be easily implemented using sessions and custom routing handlers:

phpy::on('/my.*/', function() { return $_SESSION['user_id'] > 0 ? true : false; });

This will allow access to all /my* endpoints only for users with declared $_SESSION['user_id'] variable (which should obviously be set within while processing signin form).

HomeHow to build web appDocumentationGithub@Denys Golotiuk in 2024