Cómo garantizar un estándar de codificación en PHP

¿Alguna vez te tocó trabajar sobre el código de otras personas? Apuesto a que sí.

A que es molesto encontrar cosas como esta:

<?php

if ($nombre== 'Pedro')
   echo 'Hola Pedro!';
else{
   echo 'Tú no eres Pedro!';
}

if ('Juan' ==$nombre)
   echo 'Hola Juan!';
else
   echo 'Tú no eres Juan!';

¿O no?

O tal vez te parezca lo mismo que:

<?php

if ('Pedro' == $nombre) {
   echo 'Hola Pedro!';
} else {
   echo 'Tú no eres Pedro!';
}

if ('Juan' == $nombre) {
   echo 'Hola Juan!';
} else {
   echo 'Tú no eres Juan!';
}

Si ese es el caso, puedes dejar de leer este artículo, dudo que haya algo valioso para tí en lo que queda por delante.

En mi caso, estoy convencido de que la calidad del código es tan importante como la de la funcionalidad de la aplicación que ese código sustenta.

El caso es que ambos códigos son funcionalmente equivalentes, de modo que… ¿qué es lo que está mal ahí?

El problema es que no se está utilizando un estándar de codificación.

Qué es un estándar de codificación

Un estándar de codificación es una serie de reglas que determinan cómo debe escribirse el código.

El objetivo es lograr un código fácil de leer por otros humanos (para la computadora mientras funcione todo lo demás da igual).

Un ejemplo de una regla como esta podría ser «todos los if llevan {} independientemente de que haya una línea dentro del bloque o más de una«.

Por qué es importante usar un estándar de codificación

Los estándares de codificación ayudan a disminuir el esfuerzo necesario para escribir el código: no se pierde tiempo tomando micro-decisiones como si poner o no poner las {} en cada situación.

A la vez, el uso de un estándar de codificación hace más fácil la lectura del código escrito por diferentes personas, lo que hace más sencillo el mantenimiento del código a largo plazo.

En definitiva, seguir un estándar de codificación permite disminuir la carga cognitiva que soportan los desarrolladores.

Esta es la misma razón por la que no deberías escribir tu código en spanglish.

Qué estándares de codificación existen en PHP

El estándar más ampliamente aceptado actualmente es PSR-12 pero es bastante común que cada organización cree su propio estándar, esperablemente basado en alguno pre-existente, lo cual suele derivar en una situación como la que bien ilustra este cómic de xkcd:

Cómo adaptar el código existente al estándar

Una vez se ha definido el estándar a utilizar, no suele ser complejo implementarlo en el código que se generará a partir del momento actual.

El desafío consiste en realizar las modificaciones requeridas al código pre-existente para que sea compatible con el estándar.

Aquí existen básicamente dos caminos posibles:

Opción 1: Manualmente

Opción 2: Usando php-cs-fixer.

Qué es php-cs-fixer

php-cs-fixer es una herramienta de línea de comandos escrita en php por Fabien Potencier y Dariusz Rumiński cuyo objetivo es, precisamente, el de modificar código de modo de garantizar que cumpla con las reglas definidas en un estándar de codificación.

Su uso es bastante simple, sólo requiere indicarle la ruta a la base de código sobre la que se trabajará y las reglas que se desea hacer cumplir.

Ejemplo de uso de php-cs-fixer

Partiendo de un código que no respeta el estándar PSR-12, usando este comando:

./php-cs-fixer fix -v

Se obtiene el siguiente resultado:

PHP CS Fixer 3.7.0 #StandWithUkraine️ by Fabien Potencier and Dariusz Ruminski.
PHP runtime: 8.1.3
Loaded config default from "/home/mauro/Code/car-rental-php/.php-cs-fixer.dist.php".
Using cache file ".php-cs-fixer.cache".
FSSSSSSFFFSSSFSSSSFSFSFFSFFFSSFFFFFFFFFFFFFFFFFFFF                                                                                                        50 / 50 (100%)
Legend: ?-unknown, I-invalid file syntax (file ignored), S-skipped (cached or empty file), .-no changes, F-fixed, E-error
   1) www/test_db_pdo.php (braces, single_blank_line_at_eof)
   2) www/tests/_support/Helper/Functional.php (braces, blank_line_after_opening_tag)
   3) www/tests/_support/Helper/Acceptance.php (braces, blank_line_after_opening_tag)
   4) www/tests/_support/Helper/Unit.php (braces, blank_line_after_opening_tag)
   5) www/tests/acceptance/CarDetailsCest.php (no_spaces_inside_parenthesis)
   6) www/index.php (full_opening_tag)
   7) www/templates/components/navbar.php (braces)
   8) www/templates/profile.php (braces)
   9) www/templates/register.php (indentation_type, braces)
  10) www/templates/rentals.php (braces)
  11) www/templates/car.php (elseif, braces)
  12) www/templates/signin.php (braces)
  13) www/templates/rent.php (elseif, braces)
  14) www/templates/home.php (elseif, braces)
  15) www/classes/Router.php (class_definition, braces, single_blank_line_at_eof)
  16) www/classes/Utils.php (class_definition, braces, single_blank_line_at_eof)
  17) www/classes/db/UserService.php (class_definition, braces, array_syntax, ternary_operator_spaces, single_blank_line_at_eof)
  18) www/classes/db/Database.php (class_definition, braces, method_argument_space, single_blank_line_at_eof)
  19) www/classes/db/RentalService.php (class_definition, braces, array_syntax, single_blank_line_at_eof)
  20) www/classes/Renderer.php (class_definition, braces, array_syntax, single_blank_line_at_eof)
  21) www/classes/pages/Signin.php (elseif, class_definition, braces, array_syntax, single_blank_line_at_eof)
  22) www/classes/pages/Profile.php (class_definition, braces, no_spaces_inside_parenthesis, single_blank_line_at_eof)
  23) www/classes/pages/Register.php (elseif, class_definition, braces, array_syntax, single_blank_line_at_eof)
  24) www/classes/pages/CarDetails.php (class_definition, braces, single_blank_line_at_eof)
  25) www/classes/pages/Homepage.php (class_definition, braces, no_spaces_inside_parenthesis, single_blank_line_at_eof)
  26) www/classes/pages/Rentals.php (class_definition, braces, no_spaces_inside_parenthesis, single_blank_line_at_eof)
  27) www/classes/pages/BasicPage.php (class_definition, braces, visibility_required, single_blank_line_at_eof)
  28) www/classes/pages/NotFound.php (class_definition, braces, single_blank_line_at_eof)
  29) www/classes/pages/Rent.php (class_definition, braces, no_spaces_inside_parenthesis, array_syntax, single_blank_line_at_eof)
  30) www/classes/pages/Logout.php (class_definition, braces, single_blank_line_at_eof)
  31) www/phpinfo.php (blank_line_after_opening_tag, single_blank_line_at_eof)
  32) www/test_db.php (blank_line_after_opening_tag)

Checked all files in 0.178 seconds, 14.000 MB memory used

Donde puede verse qué archivos han sido modificados y qué regla se ha aplicado a cada uno.

Cómo especificar qué reglas aplicar en php-cs-fixer

Existen dos formas de especificar qué reglas se aplicarán en una corrida en particular:

  1. Parámetros al momento de ejecutar el script
  2. Mediante un archivo de configuración

Independientemente de cuál sea el método elegido, las reglas pueden ser especificadas en forma explícita (una por una) o bien usando conjuntos pre-definidos.

Las reglas y los conjuntos se diferencian porque los últimos tienen un nombre que comienza con @.

En mi caso estoy usando este archivo de configuración (.php-cs-fixer.dist.php):

<?php

$finder = PhpCsFixer\Finder::create()
    ->in(__DIR__.'/www')
;

$config = new PhpCsFixer\Config();
return $config->setRules([
        '@PSR12' => true,
        'array_syntax' => [
                'syntax' =>
                'short'
        ],
        'full_opening_tag' => true,
    ])
    ->setFinder($finder)
;

Esto quiere decir que las reglas que se aplicarán serán las siguientes:

El código antes y después de php-cs-fixer

Aquí puedes ver las diferencias que se generaron en algunos archivos después de la corrida de php-cs-fixer:

git diff www/classes/Renderer.php

diff --git a/www/classes/Renderer.php b/www/classes/Renderer.php
index c5e2e0f..ac90cec 100644
--- a/www/classes/Renderer.php
+++ b/www/classes/Renderer.php
@@ -1,15 +1,16 @@
 <?php
 
-class Renderer {
+class Renderer
+{
+    private static $injection = [];
 
-    private static $injection = array();
-
-    public static function inject($key, $value){
+    public static function inject($key, $value)
+    {
         self::$injection[$key] = $value;
     }
 
-    public static function render($contentFile, $variables = array()) {
-
+    public static function render($contentFile, $variables = [])
+    {
         $contentFileFullPath = "../templates/" . $contentFile;
 
         // making sure passed in variables are in scope of the template
@@ -28,20 +29,19 @@ class Renderer {
             }
         }
 
-    require_once("../templates/components/header.php");
+        require_once("../templates/components/header.php");
 
-    echo "\n<div class=\"container\">\n";
+        echo "\n<div class=\"container\">\n";
 
-    if (file_exists($contentFileFullPath)) {
-        require_once($contentFileFullPath);
-    } else {
-        require_once("../templates/error.php");
-    }
+        if (file_exists($contentFileFullPath)) {
+            require_once($contentFileFullPath);
+        } else {
+            require_once("../templates/error.php");
+        }
 
-    // close container div
-    echo "</div>\n";
+        // close container div
+        echo "</div>\n";
 
-    require_once("../templates/components/footer.php");
+        require_once("../templates/components/footer.php");
+    }
 }
-
-}
\ No newline at end of file

git diff www/templates/register.php

diff --git a/www/templates/register.php b/www/templates/register.php
index 1aa92ce..aab01a6 100644
--- a/www/templates/register.php
+++ b/www/templates/register.php
@@ -1,6 +1,6 @@
 <div class="panel panel-default">
     <div class="panel-body">
-        <?php if($loginInfo == 0) { ?>
+        <?php if ($loginInfo == 0) { ?>
 
         <form class="form-horizontal" method="post" action="">
             <fieldset>
@@ -96,20 +96,20 @@
         </form>
         <br>
         <?php
-            if(isset($errors)) {
+            if (isset($errors)) {
                 foreach ($errors as $error) {
                     echo "<div class=\"alert alert-dismissible alert-danger fade in\">\n" .
-  					"<button type=\"button\" class=\"close\" data-dismiss=\"alert\">&times;</button>\n" .
-  					"$error\n" .
-				    "</div>\n";
+                    "<button type=\"button\" class=\"close\" data-dismiss=\"alert\">&times;</button>\n" .
+                    "$error\n" .
+                    "</div>\n";
                 }
             }
 
-            if(isset($success) && strlen($success) > 0) {
+            if (isset($success) && strlen($success) > 0) {
                 echo "<div class=\"alert alert-dismissible alert-success fade in\">\n" .
-  					"<button type=\"button\" class=\"close\" data-dismiss=\"alert\">&times;</button>\n" .
-  					"$success\n" .
-				"</div>\n";
+                    "<button type=\"button\" class=\"close\" data-dismiss=\"alert\">&times;</button>\n" .
+                    "$success\n" .
+                "</div>\n";
             }
         ?>

git diff www/classes/db/UserService.php

diff --git a/www/classes/db/UserService.php b/www/classes/db/UserService.php
index 148bc00..1f5dd82 100644
--- a/www/classes/db/UserService.php
+++ b/www/classes/db/UserService.php
@@ -2,9 +2,10 @@
 
 require_once('../classes/db/Database.php');
 
-class User {
-
-    public static function isUserAdmin($id) {
+class User
+{
+    public static function isUserAdmin($id)
+    {
         $query = "SELECT _id FROM admins WHERE user_id = :id";
 
         $stmt = Database::getInstance()
@@ -14,13 +15,15 @@ class User {
         $stmt->bindParam(":id", $id);
         $stmt->execute();
 
-        if ($stmt->rowCount() > 0)
+        if ($stmt->rowCount() > 0) {
             return $stmt->fetchColumn();
-        else
+        } else {
             return 0;
+        }
     }
 
-    public static function getUserInfo($id) {
+    public static function getUserInfo($id)
+    {
         $query = "SELECT first_name, last_name FROM user WHERE _id = :id";
 
         $stmt = Database::getInstance()
@@ -33,7 +36,8 @@ class User {
         return $stmt->fetch(PDO::FETCH_ASSOC);
     }
 
-    public static function getUserDetails($id) {
+    public static function getUserDetails($id)
+    {
         $query = "SELECT *, date_format(join_time, '%D %b %Y, %I:%i %p') as join_date  FROM user, address WHERE user._id = :id AND user.address_id = address._id";
 
         $stmt = Database::getInstance()
@@ -46,7 +50,8 @@ class User {
         return $stmt->fetch(PDO::FETCH_ASSOC);
     }
 
-    private static function userExists($id, $method) {
+    private static function userExists($id, $method)
+    {
         $query = "SELECT _id FROM user WHERE $method = :$method";
 
         $stmt = Database::getInstance()
@@ -56,18 +61,21 @@ class User {
         $stmt->bindParam(":$method", $id);
         $stmt->execute();
 
-        if ($stmt->rowCount() > 0)
+        if ($stmt->rowCount() > 0) {
             return $stmt->fetchColumn();
-        else
+        } else {
             return 0;
+        }
     }
 
-    public static function doesUserExist($id) {
+    public static function doesUserExist($id)
+    {
         $_id = self::userExists($id, "username");
-        return $_id>0?$_id:self::userExists($id, "email");
+        return $_id>0 ? $_id : self::userExists($id, "email");
     }
 
-    public static function verifyUser($id, $password) {
+    public static function verifyUser($id, $password)
+    {
         $query = "SELECT first_name, password FROM user WHERE _id = :id";
 
         $stmt = Database::getInstance()
@@ -80,13 +88,14 @@ class User {
         if ($stmt->rowCount() > 0) {
             $user = $stmt->fetch(PDO::FETCH_ASSOC);
 
-            return password_verify($password, $user['password'])?$user['first_name']:false;
+            return password_verify($password, $user['password']) ? $user['first_name'] : false;
         }
 
         return false;
     }
 
-    public static function insertAddress($addressArray) {
+    public static function insertAddress($addressArray)
+    {
         $fields = ['street', 'city', 'state', 'country', 'zip'];
 
         $query = 'INSERT INTO address(' . implode(',', $fields) . ') VALUES(:' . implode(',:', $fields) . ')';
@@ -95,7 +104,7 @@ class User {
             ->getDb()
             ->prepare($query);
 
-        $prepared_array = array();
+        $prepared_array = [];
         foreach ($fields as $field) {
             $prepared_array[':'.$field] = @$addressArray[$field];
         }
@@ -104,7 +113,8 @@ class User {
         return Database::getInstance()->getDb()->lastInsertId();
     }
 
-    public static function insertUser($userArray) {
+    public static function insertUser($userArray)
+    {
         $fields = ['first_name', 'last_name', 'email', 'username', 'password', 'ph_no', 'gender', 'address_id'];
 
         $query = 'INSERT INTO user(' . implode(',', $fields) . ') VALUES(:' . implode(',:', $fields) . ')';
@@ -112,7 +122,7 @@ class User {
         $db = Database::getInstance()->getDb();
         $stmt = $db->prepare($query);
 
-        $prepared_array = array();
+        $prepared_array = [];
         foreach ($fields as $field) {
             $prepared_array[':'.$field] = @$userArray[$field];
         }
@@ -135,5 +145,4 @@ class User {
 
         return $id;
     }
-
-}
\ No newline at end of file
+}

A que se ve mejor que ir archivo por archivo aplicando cada regla, ¿cierto?

Cómo garantizar la adhesión al estándar

Por último, para garantizar que algo sucede, nada mejor que una buena automatización.

En este caso, será cuestión de agregar la ejecución de php-cs-fixer a algún hook de git o al sistema de integración continua que se esté usando.

No quedan excusas para no implementar un estándar de codificación en tus proyectos php.

mchojrin

Por mchojrin

Ayudo a desarrolladores PHP a acceder mercados y clientes más sofisticados y exigentes

¿Te quedó alguna duda? Publica aca tu pregunta

Este sitio usa Akismet para reducir el spam. Aprende cómo se procesan los datos de tus comentarios.