Best Practices
A set of guidelines for developing a project using the Framework.
Extending Entity for Standalone Classes
Rule: Classes that do not extend another framework base class (e.g. AbstractPlugin, DataAccessObject, Store, ValuesObject) may extend Entity, but it is not required.
When extending Entity is useful
- You need validation helpers:
getPreparedData(),getExtendData()(see Validation) - You need event dispatching (
EntityextendsEventDispatcher) - You want consistency with framework classes:
Core,Template,Store,DefaultUser,ValuesObjectall extendEntity
Standalone classes that do not need validation or events are not required to extend Entity.
Example
// Standalone class without a framework parent → may extend Entity if validation/events needed
class PaymentProcessor extends Entity
{
public function process(array $request): array
{
$errors = [];
$fields = [
'amount' => [
Entity::OPTION_KEY_TYPE => Entity::FIELD_TYPE_FLOAT,
Entity::OPTION_KEY_REQUIRED => true
],
'currency' => [
Entity::OPTION_KEY_TYPE => Entity::FIELD_TYPE_STRING,
Entity::OPTION_KEY_DEFAULT => 'USD'
],
];
$data = $this->getPreparedData($request, $fields, $errors);
if ($errors) {
return [
'status' => 'error',
'errors' => $errors
];
}
// process...
return [
'status' => 'ok'
];
}
}
Classes that extend AbstractPlugin, ObjectPlugin, DataAccessObject, Store, etc. already inherit from Entity through that hierarchy.
Do Not Modify the Core for New Actions or Fields
Rule: Do not update the framework core when you need a new action or field type. Create it in your project instead. If it proves stable and valuable, it can be proposed for inclusion in the core in the future.
Why?
- Keeps your project upgradeable without merge conflicts
- Lets you iterate and validate the feature before core adoption
- Core additions require broader use cases and maintenance commitment
Best Practice
- Implement custom DGS actions or field types in your project (e.g. in a plugin or shared library)
- Extend or compose existing core classes rather than patching them
- When the feature is proven and widely useful, open an issue or PR to contribute it to the core
Avoid Making SQL Requests from Event Callbacks Related to UI
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
assertstatements. - 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.