Best Practices

A set of guidelines for developing a project using the Framework.

Bad Practice: Executing SQL queries directly from event callbacks triggered by UI interactions.

Why is this bad?

  • Performance Issues: Event-driven SQL queries may lead to unnecessary database load, slowing down the application.
  • Unclear Data Flow: Mixing database logic with UI events reduces code maintainability and readability.
  • Scalability Problems: Hardcoding SQL queries within UI event handlers makes it harder to scale and optimize the system.

Best Practice

  • Use events in UI and Theme layers only for modifying the interface or retrieving pre-processed data.
  • Keep data processing and database interactions within dedicated data service layers or plugins.
  • Store necessary data beforehand and let UI events access only pre-fetched data, reducing real-time database calls.

Using assert for HTML Validation (Unless Specifically Testing HTML)

Why is this bad?

  • Difficult Test Maintenance: Asserting raw HTML makes test cases harder to read and maintain.
  • Unstable Tests: Even minor changes in HTML structure (e.g., attribute order) can break tests, despite unchanged functionality.
  • Overuse of Regular Expressions: Regex is unreliable for checking HTML structure and leads to brittle tests.

Best Practice

  • If you need to check HTML structure, use an HTML parser instead of raw assert statements.
  • If testing functionality, verify data outputs or API responses rather than HTML markup.
  • Minimize reliance on UI elements in unit tests to ensure long-term stability.

Avoiding SQL in Loops and use SQL in Plugins

One of the most common performance mistakes is placing SQL queries inside loops. This leads to inefficient database access and slows down the system. Festi strongly discourages direct SQL queries in plugin methods. Instead, all database logic should be moved to an Object class that extends DataAccessObject.

/**
 * @urlRule /users/getActiveTimers/
 * @return bool
 */
public function onGetActiveTimersPost(Response &$response, Request $request) {
    try {
        $body = json_decode($request->getJson(), true);
        $childId = isset($body['child_id']) ? (int)$body['child_id'] : null;

        if (!$childId) {
            $response->status = 'error';
            $response->message = 'child_id is required';
            $response->setStatus(400);
            return false;
        }

        $timerRows = $this->core->db->getAll("SELECT id FROM timers WHERE child_id = $childId");
        $timerIds = array_map(fn($row) => $row['id'], $timerRows);

        $activeTimers = [];

        foreach ($timerIds as $timerId) {
            $timerId = (int)$timerId;

            $timerDetails = $this->core->db->getAll("SELECT * FROM timers WHERE id = $timerId");
            if (!$timerDetails) {
                continue;
            }

            $logs = $this->db->getRows("SELECT * FROM timer_logs WHERE timer_id = $timerId");

            $mappedLogs = [];
            $timerInactive = false;

            foreach ($logs as $log) {
                if ($log['action'] === 'STOP_TIMER') {
                    $timerInactive = true;
                }

                $mappedLogs[] = [
                    'id'         => (string)$log['id'],
                    'user_id'    => (string)$log['user_id'],
                    'action'     => (string)$log['action'],
                    'activity_id'=> isset($log['activity_id']) ? (string)$log['activity_id'] : null,
                    'created'    => $log['created_at'],
                    'updated_at' => $log['updated_at'],
                ];
            }

            if (!$timerInactive) {
                $activeTimers[] = [
                    'timer_id'   => (string)$timerId,
                    'timer_type' => $timerDetails['type'],
                    'child_id'   => (string)$timerDetails['child_id'],
                    'created_by' => (string)$timerDetails['created_by'],
                    'logs'       => $mappedLogs
                ];
            }
        }

        $response->status = 'success';
        $response->message = 'Successfully retrieved active timers';
        $response->timers = $activeTimers;
        return true;
    } catch (Exception $e) {
        \FestiUtils::addLogMessage("Error retrieving active timers: " . $e->getMessage());
        $response->status = 'error';
        $response->message = 'An error occurred';
        $response->setStatus(500);
        return false;
    }
}

Why is this bad?

  • N+1 Query Problem: Each loop iteration triggers additional database queries, causing exponential increase in query volume.
  • Poor Performance: Multiple small queries are less efficient than a single optimized query.
  • Network Overhead: Each database connection incurs network latency, significantly slowing down application response time.
  • Server Load: Excessive database calls increase load on both the application and database servers.
  • Scalability Issues: Performance degrades dramatically as data volume grows.

Best Practice

class TimerObject extends DataAccessObject
{
    public function getActiveTimersForChild(int $childId): array
    {
        $timers = $this->db->getRows("SELECT * FROM timers WHERE child_id = ?", [$childId]);
        $timerIds = array_column($timers, 'id');

        if (empty($timerIds)) {
            return [];
        }

        $placeholders = implode(',', array_fill(0, count($timerIds), '?'));
        $logs = $this->db->getRows("SELECT * FROM timer_logs WHERE timer_id IN ($placeholders)", $timerIds);

        $logsByTimer = [];
        foreach ($logs as $log) {
            $logsByTimer[$log['timer_id']][] = $log;
        }

        $activeTimers = [];
        foreach ($timers as $timer) {
            $timerId = $timer['id'];
            $timerLogs = $logsByTimer[$timerId] ?? [];

            $timerInactive = false;
            $mappedLogs = [];

            foreach ($timerLogs as $log) {
                if ($log['action'] === 'STOP_TIMER') {
                    $timerInactive = true;
                }

                $mappedLogs[] = [
                    'id'          => (string)$log['id'],
                    'user_id'     => (string)$log['user_id'],
                    'action'      => (string)$log['action'],
                    'activity_id' => isset($log['activity_id']) ? (string)$log['activity_id'] : null,
                    'created'     => $log['created_at'],
                    'updated_at'  => $log['updated_at'],
                ];
            }

            if (!$timerInactive) {
                $activeTimers[] = [
                    'timer_id'   => (string)$timerId,
                    'timer_type' => $timer['type'],
                    'child_id'   => (string)$timer['child_id'],
                    'created_by' => (string)$timer['created_by'],
                    'logs'       => $mappedLogs
                ];
            }
        }

        return $activeTimers;
    }
}

// In your plugin class, use:

/**
 * @urlRule /users/getActiveTimers/
 * @return bool
 */
public function onGetActiveTimersPost(Response &$response, Request $request) {
    try {
        $body = json_decode($request->getJson(), true);
        $childId = isset($body['child_id']) ? (int)$body['child_id'] : null;

        if (!$childId) {
            $response->status = 'error';
            $response->message = 'child_id is required';
            $response->setStatus(400);
            return false;
        }

        $activeTimers = $this->object->timer->getActiveTimersForChild($childId);

        $response->status = 'success';
        $response->message = 'Successfully retrieved active timers';
        $response->timers = $activeTimers;

        return true;
    } catch (Exception $e) {
        \FestiUtils::addLogMessage("Error retrieving active timers: " . $e->getMessage());
        $response->status = 'error';
        $response->message = 'An error occurred';
        $response->setStatus(500);
        return false;
    }
}

In your plugin class, use:

$activeTimers = $this->object->timer->getActiveTimersForChild($childId);
  • Only two SQL queries total (not N+1).
  • Database logic encapsulated in Object class.
  • Plugin remains clean and focused on handling requests.