How to store passwords in a database (PHP). Encryption, hashing passwords

Why you can’t store passwords in clear text

The main reason is to minimize damage in case of a database leak.

If an attacker gets only usernames and email addresses, this is bad, but not critical.

But if he has logins and passwords in his hands, he can try to use these data to log into mail services (Gmail, Yandex.Mail, Mail.ru, etc.), social networks, instant messengers, client banks, etc. …

To the same Pyaterochka personal account in order to reissue the card and spend other people’s bonuses.

In general, site users who use the same logins and passwords everywhere can get a bunch of problems.

Some developers believe that their application is well protected and there can be no leaks. There are several reasons why this opinion is wrong:

  • A developer is not a robot, he cannot help but make mistakes.
  • Hacking can happen from the side of the hosting provider, whose work is not always possible to control.
  • Incorrect server settings can lead to possible access of other hosting users to your site (relevant for virtual hosting).
  • A former work colleague may leak the database to a competitor. Maybe as revenge, or maybe just for the money.

In short, passwords cannot be stored in clear text.

Encryption and hashing

Encryption is the reversible conversion of text to a random set of characters.

Hashing is the irreversible conversion of text to a random set of characters.

The difference between these two actions is whether we can get the original string from a random set of characters according to some well-known algorithm.

I will give an example of encryption. We have a message:


I am Ram

Let’s encrypt the message using the following algorithm: shift each letter by 1 alphabetically, i.e. a turns into b , d turns into d , i turns into a . This is what the ciphertext will look like:


A GBta

Encrypted. Now, to decrypt, you need to perform the opposite operation, shift all letters 1 character back. By the way, this encryption algorithm is called the Caesar cipher ( Wikipedia).

Unlike encryption, hashing has no (or rather should not have) a way to “decrypt” a string back:

$hash = md5('
Some line');

Encrypting passwords

No need to encrypt passwords.

The decryption algorithm can be stolen or picked. When using hashing, it doesn’t matter if the attacker knows the algorithm, it doesn’t really help him in getting the original password from the hash.

Password hashing and authorization

There is a password_hash () function for hashing passwords in PHP :

$password = '123456';
$hash = password_hash($password, PASSWORD_BCRYPT);

var_dump($hash);
// string(60) "$2y$10$Vb.pry5vRGNrm6Y79UfBsun/RbXq2.XEGCOMpozrDwg.MNpfxvWHK"

The second parameter is the hashing algorithm. By default, this is the bcrypt we specified, but I recommend specifying it manually, as the basic algorithm may change in the future. It will be sad if the authorization fails on the site during the next PHP version update.

To check the correctness of the password entered by the user, use the password_verify () function :

<?php
$hash = '$2y$10$Vb.pry5vRGNrm6Y79UfBsun/RbXq2.XEGCOMpozrDwg.MNpfxvWHK';
$password = '123456';

if(password_verify($password, $hash))
    echo 'The password is correct.';
else
    echo 'The password is incorrect.';

Again. When registering a user, you need to pass the password to the password_hash () function , and save the resulting hash to the database.

When trying to authorize, we get the user by his login and check with the password_verify () function if the password hash matches the password that the user entered.

Thus, it no longer makes sense to store the original password.

Yes, different hashing algorithms generate hashes of different lengths, so it is recommended to store the hash in a VARCHAR (255) field.

MD5 and SHA1 algorithms

There are still articles on the Internet where it is recommended to hash passwords with the md5 () and sha1 () functions .

You cannot use them to hash passwords!

These algorithms are outdated long ago and are not secure . Instead, use the password_hash () function we discussed above.

XSS Protection in PHP

What is XSS

XSS is when an attacker tries to add his own javascript code through the forms on the site (feedback, checkout, etc.), which will then be executed in the browser of the admin / site manager or other users and will do things.

How it works

Let’s take a simple feedback form. 2 fields to fill in and a submit button.

<form method = "POST">
    <p> Title: <input name = "title"> </p>
    <p> Text: <textarea name = "content"> </textarea> </p>
    <p> <button name = "form"> Submit! </button> </p>
</form>

Add a PHP handler to this form that simply prints the title and text to the screen:

<? php
if (isset ($ _ POST ['form'])) {
    echo 'Title:', $ _POST ['title'], '<br>';
    echo 'Text:', $ _POST ['content'];
}
?>
<form method = "POST">
    <p> Title: <input name = "title"> </p>
    <p> Text: <textarea name = "content"> </textarea> </p>
    <p> <button name = "form"> Submit! </button> </p>
</form>

Perfectly. The code works, the entered text is displayed above the form.

Now, instead of the text, we will enter some javascript code, for example <script> alert (‘hello’) </script> .

When the form is submitted, this code will be executed by the browser. This is the security hole. Only usually we do not display the text immediately after submitting the form, but first save it to the database, then display it on different pages of the site.

Why XSS is dangerous

Having injected his script, the attacker gains access to the entire html page, can read and change it as he pleases.

In addition, the attacker gains access to the user’s browser cookies. Of course, only those that relate to the current site. He can steal cookies that are responsible for user authorization and substitute them into his browser.

Thus, he can enter the site under someone else’s account without a username and password. Of course, only if there are no other checks on the site: for browser compliance, IP-addresses, etc., although they can be faked if desired.

How to protect yourself from XSS

Fortunately for us, there is a simple universal tool – the htmlspecialchars () function or its sometimes used counterpart htmlentities () .

How it works. HTML has such a thing as entities or mnemonics . This is when I write a specific sequence of characters directly into HTML, for example & copy; , and the browser displays the symbol corresponding to this mnemonic, in this case the copyright icon ©.

Try it yourself:


<div>
The & para; <br>
Inverted question mark & ​​iquest; <br>
Multiplication sign (cross) & times; <br>
Left arrow & larr; <br>
Typographic cross & dagger;
</div>

So that’s it. When we run the htmlspecialchars () function , it takes our string and replaces some characters in it (quotes, angle brackets, etc.) with mnemonics so that the browser is guaranteed to display our string as a string without trying to execute it as code.

Those. when we enter the text <script> alert (‘hello’) </script> into our form , the htmlspecialchars () function will turn it into & lt; script & gt; alert (‘hello’) & lt; / script & gt; … Of course, the browser will no longer accept such code as javascript and will simply display it as it is.

Let’s check:

<? php
if (isset ($ _ POST ['form'])) {
    echo 'Title:', htmlspecialchars ($ _ POST ['title'], ENT_QUOTES, 'UTF-8'), '<br>';
    echo 'Text:', htmlspecialchars ($ _ POST ['content'], ENT_QUOTES, 'UTF-8');
}
?>
<form method = "POST">
    <p> Title: <input name = "title"> </p>
    <p> Text: <textarea name = "content"> </textarea> </p>
    <p> <button name = "form"> Submit! </button> </p>
</form>

Now, whatever javascript code we try to substitute, it will simply be output to the browser as a string.

When is it better to handle a string

There is often debate on the Internet about when is the best to process text, before writing to a database or when displaying it on screen.

Processing before writing to the database has several disadvantages:

  • We cannot find out the real length of the line, since LLC “Three Cats” is 14 characters, but LLC “Three Cats” – already 24 characters.
  • In addition to HTML, data from the database sometimes needs to be substituted somewhere else, for example, in word / excel / pdf files, where there may not be any mnemonics. We’ll have to decode all the data back to its original form.

In general, this is inconvenient. I recommend that you always save the source text entered by the user into the database, and process it when it is displayed.

How to simplify string handling

And what, every time now to write this long function?

<div>
    <?= htmlspecialchars($_POST['title'], ENT_QUOTES, 'UTF-8') ?>
</div>

No thanks. Better to write a separate function for this case:

function e($string)
{
    return htmlspecialchars($string, ENT_QUOTES, 'UTF-8');
}

You can replace return immediately with echo, it doesn’t matter. Now it’s much easier to process strings:


if (isset ($ _ POST ['form'])) {
    echo 'Title:', e ($ _ POST ['title']), '<br>';
    echo 'Text:', e ($ _ POST ['content']);
}

If you’ve heard about templating engines like Twig or Blade, they use their own syntax for outputting variables so that they are always processed by default:


<div> {{title}} </div> <! - With processing ->
<div> {!! content !!} </div> <! - No processing ->

Protection Against SQL Injection in PHP

What is SQL Injection

SQL injection is the substitution of such data into an SQL query that changes the structure of this query. An attacker could exploit the vulnerability to execute arbitrary SQL.

Let’s imagine a typical task – displaying articles on a website. When you go to the address /index.php?id=15 , an article should be displayed, the ID of which in the database is 15.

How beginner developers usually write a database query:

$query = 'SELECT * FROM `articles` WHERE `id` = ' . $_GET['id'];

The developer expects $ _GET [‘id’] to contain a number and the final request will be like this:

SELECT * FROM `articles` WHERE `id` = 15

But instead, an attacker can pass the string -1 OR 1 = 1 :

SELECT * FROM `articles` WHERE `id` = -1 OR 1=1

When running this query, all records will be selected instead of one, since records with negative identifiers are most likely not in the database, and the condition 1 = 1 is always true.

But the point is different. After fragment 1 = 1, an attacker can supplement the query with any arbitrary SQL code.

What can an attacker do?

It depends on the specific request, as well as how you run it.

If the query is not executed through the mysqli_multi_query () function , which supports multiqueries (multiple queries separated by semicolons), then the attacker has no way of executing a completely arbitrary query like this:

SELECT * FROM `articles` WHERE `id` = 1; DROP TABLE `articles`

This will not work, since multiple queries are not supported by default.

But an attacker can do something bad. For example, using UNION you can get any data from any tables.

Let’s imagine we have an articles table with 4 fields: id | title | content | created_at and also a users table with 3 fields: id | login | password .

Since UNION allows you to combine data from tables with only the same number of columns, an attacker can specify 2 columns he needs, and fill the remaining 2 with any values, for example, units:

SELECT * FROM `articles` WHERE `id` = -1 UNION SELECT 1, `login`, `password`, 1 FROM `users`

As a result, instead of title and content, the page will display the login and password of one of the users. And this is just one of dozens of possible hacking options.

Escaping quotes

Before moving on to the existing methods of protection, I want to separately explain what shielding is in general and why it is needed.

Let’s take this example:

$name = 'Ram';
$query = "UPDATE `users` SET `name` = '$name'";

This request is fine, it will execute as we expect:

UPDATE `users` SET `name` = 'Ram'

But what if $ name contains a single quote?

$name = "Ram Mohan";

Then the SQL query becomes like this:

UPDATE `users` SET `name` = 'Mohan'

Attempting to execute this query will result in a syntax error. To avoid it, the second quotation mark must be escaped, i.e. add a backslash to it.

We will analyze the methods of escaping and their reliability a little below, but for now, for simplicity, let’s take addslashes () :

<?php
$name = "Mohan";
$name = addslashes($name);
$query = "UPDATE `users` SET `name` = '$name'";

What will happen in the end:

UPDATE `users` SET `name` = 'Mohan'

Done, the query will run even if the quotes are present.

Quotes are not the only thing to escape. Different functions can escape different characters, we’ll talk about this in detail a little later.

And now an important point. Some developers believe that escaping is sufficient to completely protect against SQL injection.

Okay, let’s take another look at the very first example with SQL injection:

$_GET['id'] = '-1 OR 1=1';
$query = 'SELECT * FROM `articles` WHERE `id` = ' . $_GET['id'];
SELECT * FROM articles WHERE id = -1 OR 1=1

There are no quotes in this query. But there is vulnerability. Hence, we conclude that escaping does not guarantee protection against SQL injection .

Ineffective ways to protect against SQL injection

Obviously, the worst option is to have no protection against SQL injection and pass the data received from the user directly to the SQL query.

$query = 'SELECT * FROM `users` WHERE `id` = ' . $_GET['id'];

Never do that! Any data must be filtered and / or validated before being substituted into an SQL query.

1. The htmlspecialchars () function

From time to time I come across articles where authors use the htmlspecialchars () function to escapes data:

$name = "Mo'han";
$name = htmlspecialchars($name);
$query = "UPDATE `users` SET `name` = '$name'";

Is it dangerous! The thing is that the htmlspecialchars () function passes dangerous characters without escaping: \ (slash), \ 0 (nul-byte) and \ b (backspace).

Here is a complete example code to demonstrate the vulnerability:

$mysqli = new mysqli('localhost', 'root', 'password', 'database');

$login = '\\';
$password = ' OR 1=1 #';

$login = htmlspecialchars($login, ENT_QUOTES, 'UTF-8');
$password = htmlspecialchars($password, ENT_QUOTES, 'UTF-8');

$sql = "SELECT * FROM `users` WHERE `login` = '$login' AND `password` = '$password'";

$items = $mysqli->query($sql) or die($mysqli->error);

while($item = $items->fetch_assoc())
{
    var_dump($item);
    echo '<br>';
}

As a result, the SQL query will be like this:

SELECT * FROM `users` WHERE `login` = '\' AND `password` = ' OR 1=1 #'

/ Is used to escape the quote immediately after $ login. `login` = ‘$ login’ turns into` login` = ‘\’ AND `password` = ‘ . After that, any code that we write will be executed, in our case it’s just OR 1 = 1 . Add # (comment) at the end to hide the last quote.

2. Filtering by the black list of characters

For some reason I do not understand, there are still developers using blacklists of characters:

$disallow = ['~', '\'', '"', '<', '>', '.', '%'];
$name = 'Вася';

$name = str_replace($disallow, '', $name);

$query = "SELECT * FROM `users` WHERE `name` = '$name'";

All characters included in the blacklist are removed from the string before being inserted into the base.

I do not want to say that this approach will not work, but its application is a big question:

  • Why make up any lists at all when there are simpler and more reliable ways of protection?
  • You need to know all the potentially dangerous symbols.
  • What if you want to allow users to use any characters from the list?

Also, I think filtering in SQL queries is a bad idea. If there are invalid characters in the line, it is better to inform the user about them and ask them to fix them, and not just cut off some of the content.

For example, a user wants to use the login ~! Mega_! Pihar! _! 9000! ~ , And after registration it turns out that his nickname has turned into MegaPihar9000 .

I think it is better to check with the user if he likes this filtered login or if he would like to change something. In short, I’m in favor of whitelisting instead of black filtering.

3. The stripslashes () function

Rarely, but there is code that uses stripslashes () before writing to the database. Since newbies are still copying this code into their projects, I’ll explain why this function is needed.

Previously, PHP had such a thing as magic quotes ( Documentation ). If this directive was included, then all data contained in $ _GET, $ _POST and $ _COOKIE was automatically escaped.

This was done to protect newcomers who substituted data directly into SQL queries. In practice, this was not the best solution:

  • It is not very convenient when all data is escaped by default, because often they are needed in their original form.
  • Ideally, escaping should take into account the encoding of the database connection, which we’ll talk about a little later. Because of this, developers had to remove the escaping with the stripslashes () function and then escaping the data again with more appropriate functions, in the case of MySQL it was mysql_real_escape_string () .

This is why stripslashes () can be found in older tutorials. To un-escape characters and get the original string.

Since PHP 5.4, the magic quotes functionality has been removed, so there is no point in using stripslashes () before writing to the database.

4. addslashes () function

In some books, you can also find recommendations to escape data with the addslashes () function .

This function is more reliable than htmlspecialchars () because it escapes both backslash and nul bytes. However, this function is worse than mysql_real_escape_string because it does not respect the encoding of the current database connection.

Therefore, even in the documentation it is directly written that this function should not be used to protect against SQL injection.Excerpt from the documentation about the addslashes () function

Effective ways to protect

1. Function mysql (i) _real_escape_string

This function works in approximately the same way as addslashes () , only it takes into account the current encoding of the database connection.

There are two important details that you should know when using this feature.

First, you must always quote escaped data. If you do not do this, there will be no sense in escaping:


// Wrong, escaped first!
$ query = 'SELECT * FROM `articles` WHERE` id` ='. $ _GET ['id'];

// Escape
$ id = $ mysqli-> real_escape_string ($ _ GET ['id']);

// Also wrong, no quotes
$ query = 'SELECT * FROM `articles` WHERE` id` ='. $ id;

// Right
$query = "SELECT * FROM `articles` WHERE `id` = '$id'";

The second danger lies in wait for those who use some specific encodings like GBK. In this case, you definitely need to specify the encoding when establishing a connection to the base.

You can read about the problem here (blog of the developer who discovered the error), here and in more detail with examples there .

2. Casting to a number

A simple and effective way to protect numeric fields is to cast the data to a number. Example:

$_POST['id'] = '15';

$id = (int) $_POST['id'];

// Or like this:
$ id = intval ($ _ POST ['id']);
// Or for fractional numbers:
$ id = (float) $ _POST ['id'];
$query = 'SELECT * FROM `users` WHERE `name` = ' . $id;

Quotation marks are not required here, since a number will be substituted into the query anyway.

There is one caveat. As I wrote above, I do not really like the idea of ​​data filtering and here it can go sideways in terms of SEO.

Let’s say you have an online store where product page URLs look like / product / 15 , where 15 is the product ID.

If the article search algorithm is that we take the second part of the URL and convert it to a number like this:

$segments[2] = '15';
$id = (int) $segments[2];

Then you can write any characters after the number 15 (only one next character must be non-numeric), for example / product / 15abcde13824_ahaha_lol and the system will still display an article with id = 15.

3. Prepared queries

One of the best ways to protect against SQL injection. The bottom line is that the SQL query is first “prepared”, and then data is transferred to it separately.

$stmt = $db->prepare('SELECT * FROM `users` WHERE `name` LIKE ?');
$stmt->execute([$_GET['name']]);

This approach guarantees the absence of SQL injection at the time of data substitution, since the query is already “prepared” and cannot be changed.

But, as usual, the details spoil everything.

If you heroically read it all the way (no), there is an interesting claim – that prepared-query PDO can also have an encoding vulnerability.

To avoid it, you need to either turn off the emulation of prepared statements, or use only reliable encodings (for example, UTF-8), or be sure to specify the connection encoding (via $ mysqli-> set_charset ($ charset) or DSN for PDO, but not via SQL query SET NAMES).

Second detail. You need to understand that protection against SQL injection will only work if we do not substitute any data directly into the query. If the developer decides to do this:

$stmt = $db->prepare("SELECT * FROM `users` WHERE `name` = '$_POST[name]'");
$stmt->execute();

Then no prepared queries will save him.

And the third detail. Column and table names cannot be substituted into prepared queries.

// Так делать нельзя
$stmt = $pdo->prepare('SELECT ? FROM ?);

Perfectly. And now what i can do?

One of the most common options is whitelisting. Simple example:


$ _POST ['product'] = [
    'title' => 'Product name',
    'article' => 'Item number',
    'content' => 'Product Description'
];

$ allowed = ['title', 'article', 'content'];

foreach ($ _ POST ['product'] as $ k => $ v)
{
    if (! in_array ($ k, $ allowed, true))
        die ('Invalid field:'. $ k);
}

If there are a lot of fields and you do not want to drive in all of them with handles, you can simply get them all from the database ( SHOW COLUMNS FROM `products`) .

Another logical option is to validate the column names, for example, allowing only letters, numbers, and underscores.

In general, again you need to finish something manually, come up with your own query generation functions. Not comme il faut. I recommend doing otherwise.

4. Ready libraries

The developers of popular libraries are probably much smarter and more experienced than us. They thought of everything for a long time and tested it on tens of thousands of programmers. So why not?

For simple projects, Medoo or RedBeanPHP is enough , for medium projects I recommend (and always use) Eloquent , but for large projects the powerful and harsh Doctrine is best suited .

Single Page Architecture in PHP

Single point of entry

The principle of a single entry point is very simple.

The web server is configured so that all HTTP requests, regardless of their URL, are processed by the same index.php script .Redirecting all requests to index.php

The current URL can be obtained from the $ _SERVER [‘REQUEST_URI’] variable . The next step is to write your own rules for processing URLs. A simplified example:

<?php
if($_SERVER['REQUEST_URI'] === '/about')  
   echo 'About the site';
elseif ($ _ SERVER ['REQUEST_URI'] === '/ contacts')
    echo 'Contacts';
else
    echo 'Error 404';

However, there is one omission in the above diagram. After all, if the server receives a request for an existing file (style.css, script.js, logo.png, etc.), the server must send this file, and not redirect it.How a single entry point works

That’s the whole principle of a single entry point. This is how it works in popular CMS like WordPress and Opencart, in Laravel frameworks, Symfony, etc.

The only question that remains for you to decide is what to do with requests to existing folders.

I personally prefer to redirect them to index.php as well.

In fact, websites often use 2 entry points.

The first is index.php, the second is a separate script designed to work with the site through the console.

The pros of a single point of entry

  • Allows the use of CNC
  • Allows complete URL management in PHP, including storing URLs in a database
  • Scripts with configs, important functions and libraries are connected only once and become available everywhere. There is no need to duplicate their connection anywhere else.

Single entry point with Apache

To configure a single entry point, you need to add several lines to the web server config. The easiest way to do this is with the .htaccess file .

This file allows you to override Apache settings for specific sites and folders.

Add the following settings to .htaccess:


# Enable redirection
RewriteEngine On
# Do not apply to existing files files
RewriteCond% {REQUEST_FILENAME}! -F
# Do not apply to existing directories
RewriteCond% {REQUEST_FILENAME}! -D
# Redirect all requests to index.php
# L stands for Last, so mod_rewrite stops working immediately at this stage.
# In short, a slight increase in performance.
RewriteRule. * Index.php [L]

To make the redirect work for existing directories, remove the line with ! -D at the end, like this:

RewriteEngine On
RewriteCond %{REQUEST_FILENAME} !-f
RewriteRule .* index.php [L]

Done. You can get the URL of the current page from the $ _SERVER [‘REQUEST_URI’] variable .

Also on the Internet you can often find another version of the config, it differs only in the last line:

RewriteRule ^(.*)$ index.php?url_param=$1 [L,QSA]

The main difference is that the URL of the current page will be stored both in $ _SERVER [‘REQUEST_URI’] and in a separate GET parameter, in our case $ _GET [‘url_param’] , and this URL will be cleared of GET- parameters.

The QSA flag is needed because without it the GET parameters won’t work, i.e. the $ _GET array will only contain url_param and nothing else.

Which of the two options to choose is up to you, personally I like the first one more.

Single entry point with Nginx

Open the domain config and write the following rule inside the server section:

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

Simple routing

If the single entry point is configured correctly, then when visiting any non-existent URL, for example / test , the index.php file should run.

The URL of the current page is in the $ _SERVER variable [‘REQUEST_URI’]

<?php
var_dump($_SERVER['REQUEST_URI']); // /test

Now we can write a very simple router that looks at the current URL and connects the appropriate script:

<?php
$uri = $_SERVER['REQUEST_URI'];

if($uri === '/')
    require 'pages/main.php';
elseif($uri === '/about')
    require 'pages/about.php';
else
    require 'pages/error404.php';

Let’s make a couple more improvements. First, URLs often need to work regardless of the presence of GET parameters, so we strip them from the URI:

// /
about? id = 5 becomes / about
$uri = parse_url($_SERVER['REQUEST_URI'], PHP_URL_PATH);

In addition, you often need to access a specific part of the URL. To do this, let’s split the URL into parts with a slash:

$segments = explode('/', trim($uri, '/'));

The $ segments variable for the URL / products / 15 will contain an array like [0 => ‘products’, 1 => ’15’] .

Now we can easily add routes for the admin panel:

<?php
$uri = parse_url($_SERVER['REQUEST_URI'], PHP_URL_PATH);

$segments = explode('/', trim($uri, '/'));

if($segments[0] === 'admin')
{
    if($segments[1] === 'users')
        $file = 'admin_users.php';
    elseif($segments[1] === 'products')
        $file = 'admin_products.php';
    else
        $file = 'admin_404.php';
}
else
{
    if($uri === '/')
        $file = 'main.php';
    elseif($uri === '/about')
        $file = 'about.php';
    else
        $file = '404.php';
}

require 'pages/' . $file;

This is the simplest routing option. Not ideal, of course, but also not requiring knowledge of regular expressions (although no one bothers to use them) and connecting third-party libraries.

When storing URLs in a database, the routing will look something like this (the actual code depends on the library you are using to interact with the database):

<?php
$uri = parse_url($_SERVER['REQUEST_URI'], PHP_URL_PATH);
$segments = explode('/', trim($uri, '/'));

$current_page = $db->select('SELECT * FROM `pages` WHERE `url` = ?', [$uri])->first();

// If there is no article - show an error
if (! $ current_page)
    require 'pages / 404.php';
// If there is an article - connect the template in which the variable $ current_page will be available
else
    require 'pages / article.php';

Htaccess routing

Some time ago it was popular to write routing rules directly in htaccess, here are some examples:

RewriteRule ^/product-(.*)_([0-9]+).php /redirectold.php?productid=$2

RewriteRule ^products/([^/]+)/([^/]+)/([^/<WBR>]+) product.php?category=$1&brand=$2&<WBR>product=$3

RewriteRule ^news/20[0-9]{2}/[0-9]{2}/[0-9]{2}/[^/]+\.html index.php

RewriteCond %{DOCUMENT_ROOT}/name/$1.php -f
RewriteRule ^([^/]+)/([^/]+)/?$ $.php1?action=$2 [L,NC,QSA]

RewriteCond %{DOCUMENT_ROOT}/name/$1.php -f
RewriteRule ^([^/]+)/([^/]+)/([^/]+)/?$ $1.php?action=$2&id=$3 [L,NC,QSA]

This approach has several disadvantages:

  • Poor readability of rules
  • You need to know the regulars well
  • Storing routing rules in your web server settings is conceptually not a good idea

In short, don’t use this approach.

URL structure in the admin area

Typically, URL addresses in the admin panel are formed according to one of the following schemes:


/ module / action / parameter1 / value1 / parameter2 / value2
/ module / action / value1 / value2

Let’s look at a simple example right away:


/ products - view the catalog
/ products / add - add a product
/ products / update - product update
/ products / delete - delete a product

So, we see that the module here is products , and the action, for example, is add . What to do with it now?

If you are familiar with OOP and MVC, then the module for you will be the name of the class, and the action is the method of this class that you want to run. If no action is specified, then it is customary to run a method called index.

If you don’t understand anything, take the module as the name of the file to be connected, and the action as, in fact, the action to be performed.

Let’s rewrite the example we wrote in the single entry point to the new URL scheme:

<?php
$uri = parse_url($_SERVER['REQUEST_URI'], PHP_URL_PATH);
$segments = explode('/', trim($uri, '/'));

$file = 'pages/' . $segments[0] . '.php';

if(file_exists($file))
    require $file;
else
    require 'pages/404.php';

So, we take the 1st URL fragment and check if a file with that name exists in the pages folder.

Those. when going to the page / test / test2, the script will check the existence of the file /pages/test.php . If the file exists, PHP will execute this file, otherwise the file /pages/404.php will be executed .

As you can see, with this approach, we no longer need to write a mapping between URLs and PHP files. PHP itself will search for the required file in the pages folder by the first fragment of the URL.

Now all that remains is to create the pages / products.php file . Let’s make a small blank:

<?php
if(empty($segments[1]))
{
    // 
Displaying the product catalog
}
elseif($segments[1] === 'add')
{
    // 
If the request came with the POST method
    if($_SERVER['REQUEST_METHOD'] === 'POST')
    {
        // add a new product to the database
    }
    // If the request came with the GET method
    else
    {
// display the form for adding a product
    }
}

This is how action processing looks like. We look at the second fragment of the URL and look for a handler for this action. For each action (add, update, delete), you need to register a separate elseif block.

Inside the add handler, we look at which method the request came in, GET or POST. If GET – display the form, if POST – add the product.

If you don’t like nested method validation, you can do it differently. In the index.php file, save the method into a separate variable:

$uri = parse_url($_SERVER['REQUEST_URI'], PHP_URL_PATH);
$segments = explode('/', trim($uri, '/'));
$method = $_SERVER['REQUEST_METHOD'];
// ...

Then in products.php we change the template to the following:

<?php
if(empty($segments[1]) && $method === 'GET')
{
// Display the product catalog
}
elseif($segments[1] === 'add' && $method === 'GET')
{
// display the form for adding a product
}
elseif($segments[1] === 'add' && $method === 'POST')
{
    // add a new product to the database
}

Done. Yes, if you do not like the fact that the same action occurs 2 times in the code, only with different methods, you can use a slightly simplified URL scheme from the Laravel framework:


(GET) / products - display products
(GET) / products? Id = 5 - display of the 5th product page
(GET) / products / create - displaying the form for adding a product
(POST) / products / store - save product from the add form
(GET) / products / edit / 15 - displaying the form for editing a product with id = 15
(POST) / products / update - saving a product from the edit form
(POST) / products / destroy - deleting a product by its identifier in the database

Adding / admin / prefix to URL

Let’s change the index.php code a bit :

<?php
$uri = parse_url($_SERVER['REQUEST_URI'], PHP_URL_PATH);
$segments = explode('/', trim($uri, '/'));

if($segments[0] === 'admin')
{
    $file = 'pages/admin_' . $segments[1] . '.php';

    if(file_exists($file))
        require $file;
    else
        require 'pages/admin_404.php';
}
else
    require 'pages/404.php';

Now, when requesting the page / admin / products, PHP will look for a file called not products.php , but admin_products.php .

Rename the file and do not forget to replace all $ segments [1] in it with $ segments [2], since $ segments [1] now contains a module, and $ segments [2] has an action.

Advanced router FastRoute

If you’re looking for a more advanced routing system, I recommend checking out the FastRoute library . It is a very powerful router, perfect for complex applications, especially if you are using OOP.

If you want me to write a separate article on working with FastRoute – write about it in the comments.