Search code examples
phpincludeundefinedrequirecircular-dependency

Undefined variable in script with circular includes (first script includes another which includes the first one)


I'm dealing with a PHP application with what seems to have a peculiarity: One of its files (helpers.php) has a couple of functions that includes another file, and the included file (db_connection.php) includes the file that originally included it.

helpers.php:

<?php

function lineBreak()
{
    return "\n<br>\n";
}

function saveScoreToDB($score)
{
    //session_start(); // Already started

    $usuario_id = $_SESSION["usuario_id"];
    $etapa = $_SESSION["etapa"];

    try
    {
        $query_etapa = "SELECT id FROM etapas WHERE numero = $etapa";

        require_once "db_connection.php";

        // `$db_link` works perfectly fine here:
        $etapa_id = $db_link->query($query_etapa)->fetchColumn();

        $query_score = "INSERT INTO score
                            (
                                usuario_id,
                                etapa_id,
                                pontos
                            )
                                VALUES
                            (
                                $usuario_id,
                                $etapa_id,
                                $score
                            )";

        $db_link->query($query_score);
    }
    catch (Exception $e)
    {
        $_SESSION["error_message"] = $e->getMessage();
        header("Location: erro.php");
    }
}

function completeTest($redirectTo)
{
    unset($_SESSION["etapa"]);

    $usuarioId = $_SESSION["usuario_id"];

    // TODO: try/catch

    try
    {
        $queryEmailUsuario = "SELECT email FROM usuarios WHERE id = $usuarioId";
        $queryNomeUsuario = "SELECT nome FROM usuarios WHERE id = $usuarioId";

        require_once "db_connection.php";

        // `$db_link` does *not* work here. Why?
        $emailUsuario = $db_link->query($queryEmailUsuario)->fetchColumn();
        $nomeUsuario = $db_link->query($queryNomeUsuario)->fetchColumn();

        // Routine to send email using the variables above
    }
    catch (Exception $ex)
    {
        // TODO
    }
}

db_connection.php:

<?php

require_once "db_credentials.php";
require_once "helpers.php";

// Variables used here come from `db_credentials.php`
$dsn = "mysql:host=$host;dbname=$dbname;port=3307;charset=utf8;";

$options = [
    PDO::ATTR_ERRMODE            => PDO::ERRMODE_EXCEPTION,
    PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC,
    PDO::ATTR_EMULATE_PREPARES   => false
];

try
{
    $db_link = new PDO($dsn, $user, $pass, $options);
}
catch (PDOException $e)
{
    echo "Error connecting to the database.";
    echo lineBreak();
    echo $e->getMessage();
    echo lineBreak();
    echo lineBreak();
}

Notice how in the first script variable $db_link is used in two different functions, both of which include the file where this variable is defined. Within the first function (saveScoreToDB), the variable is available and the function works fine; but within the second (completeTest) it is not available and I get an undefined variable error.

Why is that? How to make it work?


Solution

  • The first require_once() works because that's the "once", but it's only in-scope in that single function call, so $db_link gets tossed out at the end of the function call and is never seen again. You can change that to require(), but creating a new connection for every single function call is... not going to work out well in the long run.

    Ideally you create the connection once and then pass it in via parameters where it is needed, eg:

    require_once('db_credentials.php');
    
    saveScoreToDB($score, $db_link);
    completeTest($redirectTo, $db_link)
    

    But that might get a bit tedious, right? Well this is where classes become useful.

    class MyThing {
    
      protected $db;
    
      public function __construct(\PDO $db) {
        $this->db = $db;
      }
    
      public function saveScoreToDB($score) {
        $this->db->prepare(...);
      }
    
      public function completeTest($redirectTo) {
        $this->db->prepare(...);
      }
    }
    
    $thing = new Mything($db_link);
    $thing->saveScoreToDB(42);
    $thing->completeTest('yes');