- Introduction
- Installation
- Configuration
- Directory structure
- Routing
- Middleware
- Controllers
- Errors and logging
- Frontend
- Database
- Domain
- Security
- Testing
- Deployment
A skeleton project for Slim 3.
This is a Slim 3 skeleton project that includes Routing, Middleware, Twig templates, Translations, Assets, Sessions, Database Queries, Migrations, Console Commands, Authentication, Authorization, CSRF protection, Logging and Unit testing.
Step 1: Create a new project:
composer create-project --prefer-dist odan/prisma my-appStep 2: Set permissions
(Linux only)
cd my-appsudo chown -R www-data tmp/
sudo chown -R www-data public/cache/Optional
NOTE: The app will have ability to create subfolders
in tmp/ and public/cache/ which means it will need 760.
sudo chmod -R 760 tmp/
sudo chmod -R 760 public/cache/NOTE: Debian/Ubuntu uses www-data, while CentOS uses apache and OSX _www.
Step 3: Setup
Run the installer script and follow the instructions:
sudo php slim installStep 4: Run it
- Open
http://localhost/my-app - Login with username / password:
admin / adminoruser / user
- Create a file
vagrantfile:
# -*- mode: ruby -*-
# vi: set ft=ruby :
Vagrant.configure("2") do |config|
config.vm.box = "ubuntu/bionic64"
config.vm.provision :shell, path: "bootstrap.sh"
config.vm.network "forwarded_port", guest: 80, host: 8765
config.vm.provider "virtualbox" do |vb|
vb.memory = "1024"
vb.customize ['modifyvm', :id, '--cableconnected1', 'on']
end
end
- Create a file:
bootstrap.sh
#!/usr/bin/env bash
apt-get update
apt-get install vim -y
# unzip is for composer
apt-get install unzip -y
# apache ant (optional)
#apt-get install ant -y
apt-get install apache2 -y
if ! [ -L /var/www ]; then
rm -rf /var/www
ln -fs /vagrant /var/www
fi
apt-get install mysql-server mysql-client libmysqlclient-dev -y
apt-get install libapache2-mod-php7.2 php7.2 php7.2-mysql php7.2-sqlite -y
apt-get install php7.2-mbstring php7.2-curl php7.2-intl php7.2-gd php7.2-zip php7.2-bz2 -y
apt-get install php7.2-dom php7.2-xml php7.2-soap -y
apt-get install --reinstall ca-certificates -y
# Enable apache mod_rewrite
a2enmod rewrite
a2enmod actions
# Change AllowOverride from None to All (between line 170 and 174)
sed -i '170,174 s/AllowOverride None/AllowOverride All/g' /etc/apache2/apache2.conf
# Start the webserver
service apache2 restart
# Change mysql root password
service mysql start
mysql -u root --password="" -e "update mysql.user set authentication_string=password(''), plugin='mysql_native_password' where user='root';"
mysql -u root --password="" -e "flush privileges;"
# Install composer
cd ~
php -r "copy('https://getcomposer.org/installer', 'composer-setup.php');"
php composer-setup.php --install-dir=/usr/local/bin --filename=composer
php -r "unlink('composer-setup.php');"
composer self-update
# Create a new project:
mkdir /var/www/html
cd /var/www/html
composer create-project --prefer-dist --no-interaction --no-progress odan/prisma .
# Set permissions
chown -R www-data tmp/
chown -R www-data public/cache/
chmod -R 760 tmp/
chmod -R 760 public/cache/
#chmod +x slim
php slim install --environment ci
vendor/bin/phpunit- Run
vagrant up - Open http://localhost:8765
- Login: username=
user, password =user - Login as admin: username =
admin, password =admin
- todo
All the app environments variables are stored in the env.php file.
The command php slim install will copy env.example.php to env.php which should have
your own variables and never shared or committed into git.
Just rename the file env.example.php to env.php.
.
├── build # Compiled files (artifacts)
├── config # Configuration files
├── docs # Documentation files
├── public # Web server files
├── resources # Other resource files
│ ├── assets # Raw, un-compiled assets such as LESS, SASS and JavaScript
│ ├── locale # Language files (translations)
│ ├── migrations # Database migration files (Phinx)
│ └── seeds # Data seeds
├── src # PHP source code (The App namespace)
│ ├── Action # Controller actions
│ ├── Console # Console commands for cli.php
│ ├── Domain # The business logic
│ ├── Type # Types, Enum Constants
│ └── Utility # Helper classes and functions
├── templates # Twig and Vue templates + JS and CSS
├── tests # Automated tests
├── tmp # Temporary files
│ ├── assets-cache # Internal assets cache
│ ├── locale-cache # Locale cache
│ ├── logs # Log files
│ ├── routes-cache # Slim router cache files
│ └── twig-cache # Internal twig cache
├── vendor # Reserved for composer
├── build.xml # Ant build tasks
├── composer.json # Project dependencies
├── LICENSE # The license
├── slim # The command line tool
└── README.md # This file
All requests go through the same cycle: routing > middleware > conroller/action > response
All the app routes are defined in the routes.php file.
The Slim $app variable is responsible for registering the routes.
You will notice that most routes are enclosed in the group method which gives the prefix to the most routes.
Every route is defined by a method corresponds to the HTTP verb. For example, a post request to register a user is defined by:
$this->get('/users', \App\Action\UserIndexAction::class);Notice: we use
$thisbecause where are inside a closure that is bound to$app;
In a Slim app, you can add middleware to all incoming routes, to a specific route, or to a group of routes. Check the documentations
In this app we add some middleware to specific routes.
Also, we add some global middleware to apply to all requests in middleware.php.
After passing through all assigned middleware, the request will be processed by a controller / action.
The Controller's job is to translate incoming requests into outgoing responses.
In order to do this, the controller must take request data, checks for authorization, and pass it into the domain service layer.
The domain service layer then returns data that the Controller injects into a View for rendering.
This view might be HTML for a standard web request; or, it might be something like JSON for a RESTful API request.
The application uses Single Action Controllers which means: one action per class.
A typical action method signature should look like this:
public function __invoke(Request $request, Response $response): ResponseInterfaceSlim framework will inject all dependencies for you automatically (via constructor injection) by passing the container instance into the constructor.
Action example class:
<?php
namespace App\Action;
use Psr\Http\Message\ResponseInterface;
use Slim\Http\Request;
use Slim\Http\Response;
class ExampleAction
{
/**
* @var LoggerInterface
*/
protected $logger;
public function __construct(LoggerInterface $logger)
{
$this->logger = $logger;
}
public function __invoke(Request $request, Response $response): ResponseInterface
{
return $response->withJson(['success' => true]);
}
}This concept will produce more class files, but these action classes have only one responsibility (SRP).
Refactoring action classes is very easy now, because the routes in routes.php make use of the ::class constant.
Depending on the settings all warnings and errors will be logged in the tmp/logs direcory.
The default logging settings for your application is stored in the config/defaults.php > logger configuration file.
Of course, you may modify this values to suit the needs of your application.
Twig is the simple, yet powerful templating engine provided by Symfony.
In fact, all Twig views are compiled into plain PHP code and cached until they are modified, meaning Twig adds essentially zero overhead to your application.
Twig view files use the .twig file extension and are typically stored in the templates/ directory.
The integrated localization features provide a convenient way to retrieve strings in various languages, allowing you to easily support multiple languages within your application.
Language strings are stored in files within the resources/locale directory.
Within this directory there should be a mo and po file for each language supported by the application.
The source language is always english. You don't need a translation file for english.
Example:
- de_DE_messages.mo
- de_DE_messages.po
- fr_FR_messages.mo
- fr_FR_messages.po
- todo: Add description how to add more languages
You may use the getLocale and isLocale methods on the App facade to determine the current locale or check if the locale is a given value:
$locale = $this->locale->getLocale(); // en_USTo parse all translation strings run:
$ ant parse-textThis command will scan your twig templates, javascripts and PHP classes for the __()
function call and stores all text entries into po-files.
You can find all-po files in the: resources/locale directory.
PoEdit is the recommended PO-file editor for the generated po-files.
You may retrieve lines from language files using the __ php helper function.
The __ method accepts the text of the translation string as its first argument.
echo __('I love programming.');Translate a text with a placeholder in PHP:
echo __('There are %s users logged in.', 7);Of course if you are using the Twig templating engine, you may use
the __ helper function to echo the translation string.
Translate a text:
{% raw %}
{{ __('Yes') }}{% endraw %}
Translate a text with a placeholder:
{% raw %}
{{ __('Hello: %s', username) }}{% endraw %}
To update all main assets like jquery and bootrap run:
$ ant update-assetsYou can add more assets in package.json or diretly via npm.
Open the file build.xml and navigate to the target update-assets
and add more items to copy the required files into the public directory.
- You may configure the database settings per server environment.
- The global default settings are stored in
config/defaults.php>$settings['db']
This application comes with cakephp/database as SQL query builder.
The database query builder provides a convenient, fluent interface to creating and running database queries. It can be used to perform most database operations in your application, and works great with MySQL and MariaDB.
For more details how to build queries read the documentation.
This skeleton project provides console access for Phinx to create database migrations.
Some basics:
Migrations are for moving from schema to schema (and back, if possible).
Seeding is the initial data setup. If you aren't at an initial (seed) state, you need a migration to change data.
Fixtures are data for testing purposes.
$ ant generate-migration$ ant create-migrationFor more details how to create and manage migrations read the Phinx documentation.
Updating the database schema with this shorthand command:
$ ant migrate-databaseIf ant is not installed on the target server, this command can also be used:
$ vendor/bin/phinx migrate -c config/phinx.phpTo populate the database with data for testing and experimenting with the code run:
$ ant seed-databaseIf ant is not installed, you can run this command:
$ vendor/bin/phinx seed:run -c config/phinx.phpYou may add more seeds under the directory: resources\seeds\DataSeed.
The command refresh-database will rollback all migrations,
migrate the database and seed the data.
Attention: All data will be lost from the database.
$ ant refresh-database
The model layer.
A distinction is actually made between collection-oriented and persistence-oriented repositories. In this case, we are talking about persistence-oriented repositories, since these are better suited for processing large amounts of data.
A repository is the source of all the data your application needs. It serves as an interface between the domain layer (Domain services) and the data access layer (DAO). According to Martin Fowler, "A repository is another layer above the data mapping layer. It mediates between domain and data mapping layers (data mappers)". A repository improves code maintainability, testing and readability by separating business logic from data access logic and provides centrally managed and consistent access rules for a data source. Each public repository method represents a query. The return values represent the result set of a query and can be primitive/object or list (array) of them. Database transactions must be handled on a higher level (domain service) and not within a repository.
Quick summary:
- Communication with the database.
- Place for the data access logic (query logic).
- This is no place for the business logic! Use domain services for the complex business and domain logic.
Here is the right place for complex business logic e.g. calulation, validation, file creation etc.
This layer provides cohesive, high-level logic for related parts of an application. This layer is invoked directly by the Controllers.
The business logic should be placed in the service classes, and we should aim for a fat model layer and thin controller layer.
Please don't prefix all service classes with *Service.
A service class is not a "utility" class.
Think of the SRP and give a service a "single responsibility".
A service classes can, and should, have several methods as long as they serve a narrow purpose.
This also encourages you to name your classes more specifically. Instead of a "User" god-class,
you might have a UserRegistration class with a few methods focusing on registration.
Q: Why would i change my UserRegistration class?
A: Because I'm changing how I register a user
A: And not because I'm changing how I assign a User to a Task. Because that's being handled by the UserTaskAssignment class.
Use it only for "small things" like Date, Money, CustomerId and as replacement for primitive data type like string, int, float, bool, array. A value object must be immutable and is responsible for keeping their state consistent Read more. A value object should only be filled using the constructor, classic setter methods are not allowed. Wither methods are allowed. Example: public function withEmail(string $email): self { ... }. A getter method name does not contain a get prefix. Example: public function email(): string { return $this->email; }. All properties must be protected or private accessed by the getter methods.
A DTO contains only pure data. There is no business or domain specific logic, only simple validation logic. There is also no database access within a DTO. A service fetches data from a repository and the repository (or the service) fills the DTO with data. A DTO can be used to transfer data inside or outside the domain.
If you have a lot of parameters that fit together, you can replace them with a parameter object. Read more
Don't use strings or fix integer codes as values. Instead use public class constants.
This application uses sessions to store the logged-in user information. If you
have to add api routes you may use JWT or a OAuth2 Bearer-Token.
The authentication depends on the defined routes and the attached middleware. You can add routing groups with Sessions and/or OAuth2 authentication. It's up to you how you configure the routes and their individual authentication.
To check user permissions, the Actions controller contains an Auth object.
Determine the logged-in user ID::
$userId = $this->auth->getUserId();Checking the user role (permission group):
$isAdmin = $this->auth->hasRole(UserRole::ROLE_ADMIN);All session based requests are protected against Cross-site request forgery (CSRF).
All tests are located in the tests/ folder. To start the unit test run:
$ ant phpunitTo debug tests with PhpStorm you must have to mark the directory tests/
as the test root source.
- Open the project in PhpStorm
- Right click the directory
tests - Select:
Mark directory as - Click
Test Sources Root - Set a breakpoint within a test method
- Right click
test - Click
Debug (tests) PHPUnit
Everything is prepared to run mocked http tests.
Please take a look at the example tests in:
tests/TestCase/HomeIndexActionTest.phptest/TestCase/HomePingActionTest.php
Everything is ready to run integration tests.
Please take a look at the example tests in:
tests/TestCase/Domain/User/UserRepositoryTest.php
There is no special mocking example available.
Just use the PHPUnit mocking functionality or install other mocking libraries you want. Feel free.
To build a new artifact (ZIP file) which is tested and ready for deployment run:
$ ant buildThe new artifact is created in the build directory: build/my_app_*.zip
To deploy the artifact to test/staging or production just upload
the zip file with a sftp client onto your server (/var/www/example.com).
Then extract the artifact into a htdocs sub-directory and run the migrations.
You can use deploy.php for this task.
- Create a directory:
/var/www/example.com - Create a directory:
/var/www/example.com/htdocs - Upload, rename and customize:
config/env.example.phpto/var/www/example.com/env.php - Upload
config/deploy.phpto/var/www/example.com/deploy.php - Make sure the apache DocumentRoot points to the
publicpath:/var/www/example.com/htdocs/public
- Upload the new artifact file
my_app_*.zipto/var/www/example.com/ - Then run
sudo php deploy.php my_app_*.zip
Example:
$ cd /var/www/example.com
$ sudo php deploy.php my_app_2019-01-29_235044.zip