About Lemon

Lemon started as simple project for learning php. Main goal was to create simple to use framework with ability to create apps in one file. After 2 versions it has complex core but is still suitable for smaller apps.

Features

  • Simple to use router
  • Safe, hackable template engine Juice
  • Simplified database manipulation
  • Debugging tools
  • Html-based Terminal layer
  • Http abstraction
  • Simple key-value caching
  • All bundled using DI container

Thanks

  • CoolFido - Psychical helping, contributting, ideas, the whole idea of creating framework from scratch (he didn't expect that to happen)
  • Mia - 'Lemon 2'
  • Quapka - Unit testing philosofy
  • Taylor Otwell - Made Laravel, which was big inspiration
  • David Grudl - Made Nette, which was aslo great inspiration
  • Nuno Maduro - Created Termwind which was used as inspiration in Terminal Component
  • Azeem Hassni - Tutorial for creating simple router
  • Marek_P - First user who made actual app in Lemon, his code was used as inspiration for new features
  • Starganzers - ❤

About Lemon

Lemon started as simple project for learning php. Main goal was to create simple to use framework with ability to create apps in one file. After 2 versions it has complex core but is still suitable for smaller apps.

Features

  • Simple to use router
  • Safe, hackable template engine Juice
  • Simplified database manipulation
  • Debugging tools
  • Html-based Terminal layer
  • Http abstraction
  • Simple key-value caching
  • All bundled using DI container

Thanks

  • CoolFido - Psychical helping, contributting, ideas, the whole idea of creating framework from scratch (he didn't expect that to happen)
  • Mia - 'Lemon 2'
  • Quapka - Unit testing philosofy
  • Taylor Otwell - Made Laravel, which was big inspiration
  • David Grudl - Made Nette, which was aslo great inspiration
  • Nuno Maduro - Created Termwind which was used as inspiration in Terminal Component
  • Azeem Hassni - Tutorial for creating simple router
  • Marek_P - First user who made actual app in Lemon, his code was used as inspiration for new features
  • Starganzers - ❤

Installation

Installation is provided via composer. Since Lemon is microframework you won't get any starting application.

$ composer require lemon_framework/lemon

Minimal version of php is 8.1.

Once you download lemon, create folder public and file public/index.php, this will be only visible file to the server. Why it's like this is discoused in other chapters.

Replit

To setup Lemon on replit, simply fork this template.

Lemon Architecture

This article covers Lemon architecture and its dedicated to people who already understand frameworks.

Lemon uses psr-4 autoload along with psr-12 coding standart and strict types. Each component lives in its own directory and most of them have its own zest (facade) located under \Lemon\ for easier usage. Main class is \Lemon\Kernel\Lifecycle, it is based on \Lemon\Kernel\Container which is simple psr-complaint DI container and has some features for managing the whole app. To make it friendlier to beginers there is ::init() which sets everything up and boots the app using register_shutdown_function.

For testing, Lemon is using phpunit and for static analysis phpstan with level 5.

Deployment

As covered in (first-app)[] Lemon is using only single file visible to the server (public/index.php) and every request that is not existing file must be redirected to this file. There are basicaly 2 ways of aproaching it. First is more punky and not recommended and that is setting index as 404 page. Second is server-specific:

TODO add server specific deployment (from laravel ig)

Getting involved

Getting involved is welcome and supported, but please read following before you start:

Reporting issues

If you find bug that wont lead to some security issues, please open issue on (github)[https://github.com/Lemon-Framework/Lemon/issues]

If you find bug that might lead to security issues please dont open issue and write it on our discord (more info in channel #report) or to email tenmajkl@proton.me

Contributing

Things like writing tests, fixing bugs or adding few methods can be done directly. Contributing to specific components is described in its README. If you want to afd bigger features that are not covered here/in specific README, open issue first or ask on our discord.

Other support

Biggest support at this point is using and sharing Lemon.

First app

Let's create first app. After we've prepared our app (as covered in (installation)[]) we can start writing in our entry point.

First, we include composer libraries.

<?php

include __DIR__.'/../vendor/autoload.php';

Now we have to initialize our app. For this there is setup-method:

<?php

use Lemon\Kernel\Application;

include __DIR__.'/../vendor/autoload.php';

Application::init(__DIR__);

Now what exactly is going on here?

As we know, in traditional php every file represents single route. In lemon there is visible only one file, to make it simpler we will be using only this file for a while (but don't worry we will split it). Every request is redirected to this file and here we have Lemon. We use Application::init() to start our app, this will setup everyrhing for us (but don't worry we will explain what it does). So everytime there is request, Lemon finds route that matches given request and returns response.

First app

Let's create first app. After we've prepared our app (as covered in (installation)[]) we can start writing in our entry point.

First, we include composer libraries.

<?php

include __DIR__.'/../vendor/autoload.php';

Now we have to initialize our app. For this there is setup-method:

<?php

use Lemon\Kernel\Application;

include __DIR__.'/../vendor/autoload.php';

Application::init(__DIR__);

Now what exactly is going on here?

As we know, in traditional php every file represents single route. In lemon there is visible only one file, to make it simpler we will be using only this file for a while (but don't worry we will split it). Every request is redirected to this file and here we have Lemon. We use Application::init() to start our app, this will setup everyrhing for us (but don't worry we will explain what it does). So everytime there is request, Lemon finds route that matches given request and returns response.

Terminal

We have initialized our app, but how do we run it?

If we run our public/index.php in terminal: php public/index.php we will get command line interface for lemon. By default you will see help.

Running our app

For running, there is command php public/index.php serve which starts php server.

More commands can be found in help.

Debugging

Lemon has 2 important debugging tools, they both work only if you enable debug mode. To do that, simply setup config value debug.debug to true.

config('debug.debug', true);

Reporter

Lemon Reporter is error handling panel. It shows everytime you get an error.

On the top you can see the Error along with hint, this hint is powered by so-called Consultant.

Under that you can see 3 tabs, the first contains file navigator stack trace. The second has more hints and the last has request.

Dumper

While debugging you want to display data on the screen, you can obviously use stuff like print_r() or var_dump(), but this isn't that good.

In lemon there are 2 functions. d() and dd(). The first just dumps data, it uses gruvbox colorscheme and it can be folded. The second dumps the data and exits whole app.

Routing

Since in Lemon we don't follow traditional aproach file=route, there is other way. As said, lemon Application automaticaly finds route depending on the reqest, but first we have to define them. For that there is class Lemon\Route. So let's define first route. This is simple definition:

<?php

use Lemon\Kernel\Application;
use Lemon\Route;

include __DIR__.'/../vendor/autoload.php';

Application::init(__DIR__);

Route::get('/', function() {
   return 'Hello world!';
});

In this example, when user sends get to path /, defined function will be executed and its result will be send back to user. Now what happens when user sends request to other path? Since the path is not registered user will receive 404 error page. However if we send request to existing path, but with method that ain't registered, we will get 400. This means we can have multiple callbacks for one path and each will be callen depending on method.

<?php

use Lemon\Kernel\Application;
use Lemon\Route;

include __DIR__.'/../vendor/autoload.php';

Application::init(__DIR__);

Route::get('/', function() {
   return 'Hello world!';
});

Route::post('/', function() {
   return 'Hello post world!';
});

At this point, if you sent POST to / and received 400, its ok, its because of CSRF protection which will be covered later.

This can be obviously applied to any method.

If we want to have route that will support all methods we can use Route::any()

Dynamic routing

While defining routes path you can add dynamic parts.

<?php

use Lemon\Kernel\Application;
use Lemon\Route;

include __DIR__.'/../vendor/autoload.php';

Application::init(__DIR__);

Route::get('/users/{user}', function($user) {
   return 'Hello '.$user;
});


In this example, if we send request to /users/Fido/ we will see Hello Fido. So dynamic routing is basicaly about creating parts in route that user can fill with everyrhing they want. Content of the path will be passed into to function as argument. We can have multiple parts like this.

Now let's talk about http.

HTTP abstraction

Lemon has builtin http abstraction which helps you work with request and response.

Request

Class \Lemon\Http\Request is created from sent data automaticaly before we our app is booted.

While creating routing callback, we can inject this request into the function (injecting will be covered in lifecycle chapter), we simply do that by specifying the type in the function.

Route::post("/login", function(\Lemon\Http\Request $request) {
//... 
});

The thing that matters here is the type, not order so we can use it with dynamic parameters which are using name to pass it.

Route::post("/posts/edit/{post}", function(\Lemon\Http\Request $request, $post) {
//... 
});

// will work the same as

Route::post("/posts/edit/{post}", function($post, \Lemon\Http\Request $request) {
//... 
});

Working with request

The most basic things can be reached using properties, let's say we've sent post to /posts/edit/foo?darkmode=true

Route::post("/posts/edit/{post}", function($post, \Lemon\Http\Request $request) {
    $request->path // /posts/edit/foo
    $request->query // ?darkmode=true
    $request->method // POST
    $request->headers // ["Content-Type" => "application/x-www-form-urlencoded"]
    $request->body // title=something
    $request->cookies // ["CSRF_TOKEN" => "lmoyiVzP82ZvSUSXx0FaOvpwdv1h4uxj"]
    $request->ip // 127.0.0.1
});

Obtaining data

To obtain data there are several methods.

To receive data from query there is query() method. This method returns either single value or all data as array. So let's say we've sent request to /?foo=bar&bar=baz

$request->query(); // ["foo" => "bar", "bar" => "baz"]
$request->query("foo"); // "bar"

While obtaining data from body (e.g. in POST) we can either get all the data as array with method data() or get single value using either method get(key) or just as property.

$request->data(); // ["foo" => "hello", "bar" => "baz"]
$request->get("foo"); // "hello"
$request->foo; // "hello"

You can also check if value exists using has(key)

$request->data(); // ["foo" => "hello"]
$request->has("foo"); // true
$request->has("bar"); // false

Data from body are parsed automaticaly depending on content type.

You can also check the content type using is(type)

Working with headers

Headers operarions are similar to body:

$request->headers(); // ["Content-Type" => "application/x-www-form-urlencoded"]
$request->hasHeader("Content-Type"); // true
$request->hasHeader("foo"); // false
$request->header("Content-Type"); // application/x-www-form-urlencoded

Cookies

And the same goes for cookies

$request->cookies(); // ["CSRF_TOKEN" => "lmoyiVzP82ZvSUSXx0FaOvpwdv1h4uxj"]
$request->hasCookie("CSRF_TOKEN"); // true
$request->hasHeader("foo"); // false
$request->cookie("CSRF_TOKEN"); // lmoyiVzP82ZvSUSXx0FaOvpwdv1h4uxj

Response

There are several classes representing response with different body. All of them inherit class \Lemon\Http\Response. Each class acts differently while sending body.

Creating Response in routing

Response is automaticaly created from return type of routing callback using ResponseFactory.

If you return scalar type (string, bool, float, int) it will act normaly - just display it.

Returning array or class that implements \Lemon\Http\Jsonable will send it as json (with content-type application/json).

Route::get('/api/users', function() {
    return ['majkel', 'coolfido'];
});

If you return template (see templating) it will render it.

Route::get('/home', function() {
    return template('home');
});

You can also return any Response object which is handy if we want to tweak the response.

Creating Response Manualy

As said before, class \Lemon\ResponseFactory is able to create route using callback. But not just that. This was done thanks to ::make() method, there is another similar, which instead of callback takes any value and its called ::resolve().

Another method is error() this method takes http status code and returns http error response.

Route::get('/home', function() {
    return \Lemon\ResponseFactory::error(401);
});

This will send simple 401 status page.

To create own pages you can simple create templates. They will be stored in templates/errors and each named as code.juice.

Also, you can create handlers, theese will be executed to create the response similary as routing callbacks.

\Lemon\Response::handle(401, function() {
    return 401;
});

Route::get('/home', function() {
    return \Lemon\ResponseFactory::error(401);
});

Now we will see just 401.

To make it simpler you can use error() function.

To create redirect, you can use function redirect(path). This method returns RedirectResponse which is dedicated to redirecting.

Also, you can create all of this by hand, let's see all default responses. They are all located under \Lemon\Http\Reponses\

EmptyResponse wont contain any body, its mostly returned by middlewares.

HtmlResponse is most basic response which contains html body that will be send.

JsonResponse contains body that will be send as json.

RedirectResponse is similar to EmptyResponse but is used for redirecting.

TemplateResponse is similar to HtmlResponse but before sending it renders template from its body.

TextResponse sends body as plain text.

Manipulating response

You can set basicaly every parameter of response.

Theese are methods to work with headers.

$response->header('Content-Type', 'text/html'); // sets given header to given value
$response->header('Content-Type'); // returns value of given header (in this case text/html)

$response->location('/'); // Sets location header to /
$response->redirect('/'); // alias to location

To work with status code there is code() method

$response->code(404) // sets code to given value (404)
$response->code() // returns status code (404)

To set body there is body() method

$response->body('Hello!');

To send cookie there is cookie() method.

$response->cookie('REMEMBER', true, 10000); // sets cookie REMEMBER to true for 10000 seconds

To sum it up, if we want to for example send json with status code 404, we can do that by:

return \Lemon\Response::resolve(['code' => 404])
    ->code(404)
;

We just make response (this is best way as it directly returns the response and you don't have to mind all the response objects) and then edit it.

Templating

Php itself was designed as template engine, problem is that to make it safe you have to be carefull and its more work, Juice solves that. Juice is Lemons template engine with syntax similar to php. Theese templates can't contain any logic and they are only for displaying content. Juice templates are transpiled into clear php and saved which means they are as fast as writing regular php.

One of the key features of Juice is hackability. It allows changing syntax and by default it has multiple "syntax packages". Theese can be set using config.

By default there are syntax packs for juice, twig-like and blade. To change it, simply change config value.

use Lemon\Templating\Juice\Syntax;

config('templating.juice.syntax', Syntax::blade());
config('templating.juice.syntax', Syntax::twig());

More about hacking in hacking/templating

Creating templates

Templates by default are stored in folder templates outside of folder public. Every trmplate has extension .juice

Displaying templates

To display template, simply return Template instance, this can be obtained with either static method \Lemon\Template::make('name', variables) or function template('name', key: value) where name doesn't have extension and . will be replaced with directory separator. Variables you pass (in static method as array, in function using named arguments) will be then available in the template.

Another way is to create route with only template using \Lemon\Route::template(path, template) where if template is missing it will use the path as path to the template.

Syntax

Syntax can be divided in two parts, output and directive.

Syntax highlighting

Vim

Download official plugin

Other editors

For most editors, html highlighting should work.

To enable html highlighting on github, simply put this into .gitattributes:

*.juice linguist-language=html

Output

In raw php you would do something like <?php echo $variable ?> however this is unsafe because if our variable has some html content inside it will be rendered which can lead to (xss attack)[https://en.wikipedia.org/wiki/Cross-site_scripting?wprov=sfti1]. In Juice you can use {{ content }} syntax which thanks to context-aware escaping (concept from nette) is lot safer as it escapes differently in different parts of html.

If you for some reason don't want to use escaping, there is {! content !}. But use it only if you know for 100 % what you are doing.

Directives

Directives are basicaly control structures such as conditions, etc. They use { } syntax and they are like <?php ?> with exception that dey dont use braces. They are divided into pairable (those which must be closed) and unpairable. Closing tags use one of / or end with name of directive that is being closed.

Conditions


{ if $foo === 1 }
    foo
{ elseif $foo > 1 } 
    bar
{ else }
    baz 
{ endif }

Instead of { endif } you can use { /if } which as covered before also applies to every pair directive.

For { if !something } there is syntax sugar { unless something }

Loops


{ foreach $foo as $item }
    ...
{ /foreach }

{ while $1 }
    ...
{ /while }

{ for $i = 0; $i < 100; ++$i }
    ...
{ /for }

Switch


{ switch $foo }
    foo
{ case 1} 
    bar
{ case 2 }
    baz 
{ endswitch }

Include

Include directive supports to add content of another template into curent.


{ include 'foo' }

Layout

Layout are basicaly templates for templates, you define layout template with yields which are spots that will be replaced with blocks in template that extends this layout.

{-- layout.juice --}

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>{ yield 'title' }</title>
</head>
<body>
    { yield 'content' }
</body>
</html>

{-- home.juice --}

{ extends 'layout'}

{ block 'title' } Home { /block }

{ block 'content' }
    Welcome!
{ /block }

Csrf

In previous chapters you might get 400 status code while sending post request on post route. This is because of lemon by default protects your page from csrf vulnerability. To protect you, Lemon on every request creates csrf token, saves it to session and on post request (and other) checks the session token with sended and if it doesn't match it throws 400. So this means we have to send the csrf token in every form. To do this, simply include { csrf } in every form:


<form method="POST">
    { csrf }
    <input type="text" name="name">
    <input type="password" name="password">
</form>

How to turn it of?

Its common that in e.g. API you don't want csrf protection. To do that, you need to include all the routes in collection and then exclude it. Collections will be covered in next chapters, so for now just follow this patern:


Route::coolection(function() {
    // routes without csrf protection
    Route::post('no-csrf-route', fn() => 'use only for apis');
    // etc
})->exclude(\Lemon\Protection\Middlewares\Csrf::class);

Configuration

Most of lemon components are using configuration. This can be accesed via \Lemon\Config or function config().

Setting config value:

\Lemon\Config::set('service.bar.baz', true);
config('service.bar.baz', true);

Now lets cover the key, as you can see it consists of words separated with dots. The first word is service which the other words belong. Ant the other words are nested keys, so in config like this:

[
    'bar' => [
        'baz' => false
    ]
]

bar.baz would be false.

Getting config value

\Lemon\Config::set('service.bar.baz');
config('service.bar.baz');

Getting file

\Lemon\Config::file('service.bar.baz');

This will return path to file under this key from perspektive of the project folder.

Loading from files

We can use multiple config files, where each will represent each service. Load them using load() method where the argument is config directory.

Database

Working safely with database is a bit painful. Lemon shrinks all the lines to make it work to just one line.

Configuring

First of all we have to configure it.

To do that simply start filling .env

Warning

Dont create .env in folder public, make in the parent folder.

First of all we have to setup database driver.

DB_DRIVER=sqlite

Sqlite

To configure sqlite, simply set database file.

DB_DRIVER=sqlite
DB_FILE=database.sqlite

Mysql

To configure mysql, you have to provide host, port, database name, user and password.

DB_DRIVER=mysql
DB_HOST=localhost
DB_PORT=3306
DB_NAME=web
DB_USER=root
DB_PASSWORD=*****

You can also provide DB_UNIX_SOCKET and DB_CHARSET, they both have default value so you dont have to provide them.

Postgre

Postgre has same configuration as mysql, however instead of DB_UNIX_SOCKET and DB_CHARSET we can configure DB_SSLMODE, which is set to prefer by default.

Running queries

All database manipulation is done by class \Lemon\DB.

To run query, simply use query() method.

\Lemon\DB::query('SELECT * FROM users');

This method automaticaly connects, keeps one connection for the request and prepares the statement, so its safe.

If you want to add some data inside the query, don't use string interpolation. Just don't do that. Do it like this:

\Lemon\DB::query('SELECT * FROM users WHERE name=?', $name);

We can also use named parameters instead of ?

\Lemon\DB::query('SELECT * FROM users WHERE name=:name', name: $name);

And thats all, one line, less pain, safe.

Validation

Lemon has tool that simplifies validating. Let's start by validating request.

To validate request, there is method validate().

$validation = $request->validate([
    'name' => 'max:16'
]);

if (!$validation) {
    return template('foo', error: 'Bad data');
}

In this method we provide validating rules. Theese rules are in format request-key => rule. If some rule doesn't pass, this method returns false. If the key does not exist, it also returns false.

So if we send request with data name=foo, it will pass, however, if we send name=foobarbazfoobarbazfoobarbaz or foo=barr, it won't.

All the rules are separated by | and arguments for the rules are provided using :.

Let's see all the rules.

RuleDescription
numericValue is number
notNumericValue is not number (but it can contain numbers)
emailValue is email
urlValue is url
colorValue is hexadecimal color
max:numberValue is smaller than given number
min:numberValue is bigger than given number
regexValue matches regex
notRegexValue doesn't match regex
containsValue contains regex
doesntContainValue doesn't contain regex

Custom rules

To create custom rule, we first have to access the rules, to do that simply use \Lemon\Validator::rules() method. and then add the rule using rule() method.

\Lemon\Validator::rules()->rule('upperCase', function(string $target) {
    return ctype_upper($target);
});

Digging deeper

Its time to go deeper, now we will try to split our app into more files and understand the whole core. Let's start with lifecycle

Lemon Lifecycle

As we know, brain of every application is class \Lemon\Kernel\Application. Till now we've been using Application::init() which works for smaller apps, however its better to split it into multiple function calls.

So what does it function actualy do?

Application::init() does process called "bootstrapping", it setups the app and automaticaly boots it. We will setup the app in separate file outside of folder public, and use index.php to only run the app. So at first, let's create file init.php.

Now, lets include all composer libraries.

include __DIR__.'/vendor/autoload.php';

Then, create application instance.

include __DIR__.'/vendor/autoload.php';

use Lemon\Kernel\Application;

$application = new Application(__DIR__);

After that, we have to load all services. This requires another key concept called Dependency injection.

Dependency injeciton

Dependency injeciton is way of managing multiple components of single app and it works like this. We have central class, called container, in this case its Application, but Application actually inherits \Lemon\Kernel\Container.

So what is the container?

Container is basicaly class which automaticaly creates instances.

Let's see it on example


class Foo
{
    public function sayHi(string $name): string
    {
        return 'Hi '.$name;
    }
}

$container = new \Lemon\Kernel\Container();

$container->add(Foo::class);

echo $container->get(Foo::class)->sayHi('CoolFido'); // Hi CoolFido

In this example, you can see that we've added service Foo to the container and then get it. The most important thing is, that instance of service we want to obtain is created only once and when we first get it.


class Foo
{
    public function sayHi(string $name): string
    {
        return 'Hi '.$name;
    }
}

class Bar
{

}

$container = new \Lemon\Kernel\Container();

$container->add(Foo::class);
$container->add(Bar::class);

echo $container->get(Foo::class)->sayHi('CoolFido'); // Hi CoolFido
echo $container->get(Foo::class)->sayHi('Majkel'); // Hi Majkel

In this example we've added another service Bar, but since we've never accessed it, the instance will never be created. We can also see that we've used the same class twice, and in both examples it was the same instance.

But the most important of this, that we can actually inject other services.

If we want to for example use our method sayHi inside class Bar. we simply inject it via type in constructor.


class Foo
{
    public function sayHi(string $name): string
    {
        return 'Hi '.$name;
    }
}

class Bar
{
    public function __construct(
        private Foo $foo
    ) {

    }

    public function sayHiLouder($name): string
    {
        return $this->foo->sayHi($name).'!!!';
    }
}

$container = new \Lemon\Kernel\Container();

$container->add(Foo::class);
$container->add(Bar::class);

echo $container->get(Foo::class)->sayHi('CoolFido'); // Hi CoolFido
echo $container->get(Bar::class)->sayHiLouder('Majkel'); // Hi Majkel!!!

And since every service in lemon is written like this (other services are injected) we have to add all of them into our container (which is in this case Application). luckily, there is method that automaticaly loads it.

include __DIR__.'/vendor/autoload.php';

use Lemon\Kernel\Aplication;

$application = new Application(__DIR__);

$application->loadServices();

We can also inject into callbacks such as route actions.

Zests

So if we want to use some service, we can inject it, but while reading you've came across Zests. Zests are nothing but classes that let you staticaly call methods on services. And every service in lemon has its Zest. Every zest is located right under \Lemon namespace thanks to which they are simpler to reach.

But to use them, we have to load them.

include __DIR__.'/vendor/autoload.php';

use Lemon\Kernel\Aplication;

$application = new Application(__DIR__);

$application->loadServices();

$application->loadZests();

Another important step is handling errors, to do that, simply load lemon handler using loadHandler().

include __DIR__.'/vendor/autoload.php';

use Lemon\Kernel\Aplication;

$application = new Application(__DIR__);

$application->loadServices();

$application->loadZests();

$application->loadHandler();

Now we just have to return the app.

include __DIR__.'/vendor/autoload.php';

use Lemon\Kernel\Application;

$application = new Application(__DIR__);

$application->loadServices();

$application->loadZests();

$application->loadHandler();

return $application;

In this file you can also do other bootstrapping stuff such as loading config or routes.

Now in our index file we simply require init file and boot our app.


<?php

use Lemon\Http\Request;

/** @var \Lemon\Kernel\Application $application */
$application = include __DIR__.'/../init.php';

$application->add(Request::class, Request::capture()->injectApplication($application));
$application->alias('request', Request::class);

$application->boot();

We also have to capture the request and add it to the container.

Now what about routes? Where to put them? Lets see routing in depth

Routing in depth

Now, when our index is there just to run our app, we have to move the routes.

To do that, create folder routes (again, outside of public). And in the folder create file that will contain main routes, for example web.php.

This file will contain routes created the same as before. But we have to somehow load this file. So go back to init.php and add

/** @var \Lemon\Routing\Router $router */
$router = $application->get('routing');

$router->file('routes.web')
    ->middleware(Csrf::class)
;

return $application;

Its important to add the CsrfMiddleware there. But wait, what is Middleware?

Middlewares

Middleware is basicaly method that is executed with route action. If the middleware returns response, it will be used instead of the one that is created from the route action.

Middlewares are typicaly located in src/Middlewares, so create this folders. Also we have to load src, but because this folder will have only classes we can use composer autoloader. So add this to composer.json and type composer dump-autoload

    "autoload": {
        "psr-4": {
            "App\\": "src"
        }
    }

Now let's create our first middleware, for example middleware that lets only authenticated people


namespace App\Middlewares;

class Auth
{
    public function onlyAuthenticated(Session $session)
    {
        if (!$session->has('name')) {
            return redirect('/login');
        }
    }
}

Here we can see middleware that checks if session key exists, if not, it redirects the user to 'login' (returns RedirectResponse), however if the key is set, it returns nothing, which means it will go to another middleware.

We can also inject "prototype" of the response, which means we get response that will be send, this is useful if we want to for example only change headers.

To add middleware to route, simply use middleware() method.

// routes/web.php

use \App\Middlewares\Auth;

Route::template('/', 'home')->middleware(Auth::class);

Controllers

Until now we had all the logic of the routes implemented inside route callbacks. This works, but in longer term is better to use controllers.

Lets say we have route login with get and post method:

Route::get('/login', fn() => template('login'));

Route::post('/login', function(Request $request) {
    // login logic
});

But this can get messy, so lets introduce controller.

Controllers live in similar folder as middlewares: src/Controllers.

Our controller will then look like this:

<?php

namespace App\Controllers;

use Lemon\Http\Request;

class Login
{
    public function get()
    {
        return template('login');
    }

    public function post(Request $request)
    {
        // login logic
    }
}

Now we have to register it, we can to it simply like this:

Route::get('/login', [Login::class, 'get']);

Route::post('/login', [Login::class, 'post']);

There is also method called Route::controller() which takes base url and controller class and creates routes depending on methods:

Route::controller('/login', Login::class);

This does the same thing. This method registers action by its request method.

You can also use so-called resources which are compatible with this shortcut. This is concept from Laravel which is basicaly standart of naming routes and methods:

// TODO table from laravel

Route::controller() returns collection of routes, which has some powerfull improvements. But what are collections?

Collections

Collection is basicaly set of routes, but also collections. Every collection has rules that apply to every route or collection inside.

Let's create collection. We can do this by Route::collection() method. This method takes function which defines all the routes:

Route::collection(function() {
    Route::get('/', fn() => 'foo');
});

This method returns collection which can be modified.

Adding middlewares

To apply middleware to every route (and routes in inner collections) in collection you can use ->middleware() method. Also, middlewares are not actualy set, it assigns it to route when needed, which means better performance.

Route::collection(function() {
    // Found route here have middleware Foo
    Route::get('/foo', fn() => 'foo');
})->middleware(Foo::class);

Excluding middlewares

If we have nested collections, we can exclude middlewares. This is handy if e.g we load file with middleware but we don't want to use it in some routes, we can exclude it:

Route::collection(function() {
    Route::collection(function() {
        // Found route here don't have middleware Foo
        Route::get('/bar', fn() => 'bar');
    })->exclude(Foo::class);

    // Found route here have middleware Foo
    Route::get('/foo', fn() => 'foo');
})->middleware(Foo::class);

Prefixes

Prefixes are strings that are before every route in collection. The main reason to use it is performance. When lemon searches for route, it checks prefix and if request url doesnt match given prefix, the collection will be skipped.

Route::collection(function() {
    // When we send request /foo all of theese routes will be skipped 
    Route::get('/', fn() => 'foo');
})->prefix('api');

Terminal in depth

Untill we started spliting our app, we were able to use index as cli - php public/index.php but this doesn't work anymore, we have to implement this by our own. Create file lemonade, actually you can name it as you want. This file will be our cli. We have to put here code that will actually run our commands, based on Terminal component.

#!/usr/bin/php
<?php

/** @var \Lemon\Kernel\Application $app */
$app = include __DIR__.'/init.php';

// --- Loading commands ---
$app->loadCommands();

$app->get('terminal')->run(array_slice($argv, 1));

This code takes terminal component from app and runs it with arguments.

Now if we do php lemonade it will work as we know.

Custom commands

You can also add your own commands. To do that, create file with commands (e.g commands.php) and load it:

#!/usr/bin/php
<?php

/** @var \Lemon\Kernel\Application $app */
$app = include __DIR__.'/init.php';

// --- Loading commands ---
$app->loadCommands();

require __DIR__.'/commands.php';

$app->get('terminal')->run(array_slice($argv, 1));

To define command, use Terminal::command():

<?php

use Lemon\Terminal;

Terminal::command('say:hi {name}', function($name) {
    echo 'Hi '.$name;
}, 'says hi');

{name} is required command argument, to make optional, use {name?}. Then there is function which is executed and description.

Now, if we type php lemonade say:hi name=majkel we will get Hi majkel.

Html rendering

To make our commands more beautiful, we can render text with colours. Regulary, to do e.g red text, you would have to do \033[33mHello\033[0m. Lemon provides html rendering inspired by termwind.

To render you have to use $this->out():

<?php

use Lemon\Terminal;

Terminal::command('say:hi {name}', function($name) {
    $this->out("<div class='text-red'>Hi {$name}</div>");
}, 'says hi');

Supported tags: - div - h1 - hr - b - i - u - br - p

Supported classes: - text-black - text-red - text-green - text-yellow - text-blue - text-magenta - text-cyan - text-white - bg-black - bg-red - bg-green - bg-yellow - bg-blue - bg-magenta - bg-cyan - bg-white

Caching

Cors

Translating

Lemon provides simple way of translating your application. We simply define all texts of our webbsite in translation files and then, instead of writing all the text, we just enter what text we want and lemon will write it in curent language.

Configuring

Create new config file config/translating.php (see configuring for more information).

And fill it with this:

<?php

declare(strict_types=1);

return [
    'directory' => 'translations',
    'fallback' => 'en',
];

Where directory is folder of all your translations and fallback is default language.

Creating translation files

Translation file is php file that returns all texts. It is contained in configured directory and its name is language name e.g en or cs.

Example of translations/en.php:

<?php

return [
    'title' => 'Welcome to the Lemon Framework',
    // etc.
];

Example of translations/cs.php:

<?php

return [
    'title' => 'Vitejte v Citronove Ramopraci',
    // etc.
];

Setting language

To setup language simply use \Lemon\Translator::locate() with language as string. Be aware that this will set language only for curent request. To make it more long-lasting, save it into session and use dedicated middleware.

Middleware \Lemon\Translating\Middlewares\TranslationLocalizer automaticaly sets language from session key locale.

Example of language-setting route:

Route::get('lang/{$lang}', function($lang) {
    \Lemon\Session::set('locale', $lang);
});

Route::collection(function() {
    // every route in this group will have middleware that automaticaly sets language from session
    Route::get('/', fn() => template('home'));
})->middleware(\Lemon\Translating\Middlewares\TranslationLocalizer::class);

When we send get to url lang/cs our language will be set to czech. Once we go to / it will load our language from session and our web will use czech translations.

Retreiving texts

OK, we know how to set the language, but how to actualy retreive texts. We can either use function text() so if we do something like:

Route::get('title', function() {
    return text('title');
});

and depending on curently set language we will see either Welcome to the Lemon Framework or 'Vitejte v Citronove Ramopraci'

In templates, there is directive { text } which does basicaly the same but you dont need to use quotes:

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>{ text title }</title>
</head>
<body>
    ...
</body>
</html>

Hacking juice

Juice was designed to be hackable. Let's see how it actualy works and how you can hack it. This part refers to sourcecode, so its recommended to have oppened it.

Juice transpiler has 2 parts, lexer and parser, you can technicaly write your own transpiler based on Juice and write own lexer/parser.

Lexer

Lexer uses regular expressions to convert template into array of tokens. Theese regexes are loaded from config from templating.juice.syntax as class \Lemon\Templating\Juice\Syntax.

Parser

Parser takes all the tokens and iteratively converts each token back into php file. Text tokens are rendered as they are, but their context is determined so we can have context-aware escaping.

Output tokens are parsed using \Lemon\Templating\Juice\Compilers\OutputCompiler.

Directives are parsed using \Lemon\Templating\Juice\Compilers\DirectiveCompiler and each lives in its own class.

Adding directive

Using method Compiler::addDirective() (which is also service so \Lemon\Juice::addDirective()) we can add custom directive. Every directive must implement \Lemon\Templating\Juice\Compilers\Directives\Directive, if it has method compileClosing its closable directive.

Hacking juice

Juice was designed to be hackable. Let's see how it actualy works and how you can hack it. This part refers to sourcecode, so its recommended to have oppened it.

Juice transpiler has 2 parts, lexer and parser, you can technicaly write your own transpiler based on Juice and write own lexer/parser.

Lexer

Lexer uses regular expressions to convert template into array of tokens. Theese regexes are loaded from config from templating.juice.syntax as class \Lemon\Templating\Juice\Syntax.

Parser

Parser takes all the tokens and iteratively converts each token back into php file. Text tokens are rendered as they are, but their context is determined so we can have context-aware escaping.

Output tokens are parsed using \Lemon\Templating\Juice\Compilers\OutputCompiler.

Directives are parsed using \Lemon\Templating\Juice\Compilers\DirectiveCompiler and each lives in its own class.

Adding directive

Using method Compiler::addDirective() (which is also service so \Lemon\Juice::addDirective()) we can add custom directive. Every directive must implement \Lemon\Templating\Juice\Compilers\Directives\Directive, if it has method compileClosing its closable directive.