Create New Module
A module is an independent PHP class that lives under modules/{name}/.
Modules work without modifying theme files — all templates stay inside the module folder.
AI-assisted development:
Download the full developer guide and upload it to any AI assistant (ChatGPT, Claude, Cursor, etc.):
github.com/bera65/frisay/blob/main/modules/MODULE_DEVELOPER_GUIDE.md
Once the AI has read this file, it understands the full module architecture, naming rules, API patterns, security requirements, and helper classes — so you can describe your module in plain language and generate working code much faster.
Download the full developer guide and upload it to any AI assistant (ChatGPT, Claude, Cursor, etc.):
github.com/bera65/frisay/blob/main/modules/MODULE_DEVELOPER_GUIDE.md
Once the AI has read this file, it understands the full module architecture, naming rules, API patterns, security requirements, and helper classes — so you can describe your module in plain language and generate working code much faster.
What Can a Module Do?
- Admin configuration screen (
adminPage()) - Storefront HTML injection via display hooks — see Hook List
- JSON API endpoints (
api/{action}.php) - Custom store pages via
$routes - Payment method integration (checkout)
- Database tables via
install.sql/uninstall.sql
Module File Schema
modules/{name}/
{name}.php ← Main class (REQUIRED)
logo.png ← Admin module list icon
install.sql ← Install SQL (optional)
uninstall.sql ← Uninstall SQL (optional)
assets/
templates/
admin/
admin.tpl ← Admin configure page
front/
{hook_name}.tpl ← Storefront hook output
css/
front.css ← Storefront styles
admin.css ← Admin styles
js/
front.js ← Storefront scripts
admin.js ← Admin scripts
img/ ← Images
api/
{action}.php ← API endpoint files
front/
{page}.php ← Custom store route file
Naming Rules (Critical)
| Item | Rule | Example |
|---|---|---|
| Folder name | Lowercase, hyphens allowed | my-module |
| Main file | {name}/{name}.php | modules/my-module/my-module.php |
| PHP class | PascalCase + Module | my-module → MyModuleModule |
| Admin URL | Auto-generated | /admin/module-my-module |
| API URL | Via module router | /api/module.php?m=my-module&action=save |
$name property | Must match folder name exactly | public string $name = 'my-module'; |
ModuleBase — Key Properties
| Property | Purpose |
|---|---|
$displayHooks | Supported hooks: hook_name => description |
$defaultDisplayHooks | Hooks auto-assigned on install() |
$apiActions | action => api/file.php map |
$routes | slug => front/page.php custom store pages |
$frontStylesheets / $frontScripts | Assets loaded on storefront (empty = load all files in folder) |
$isPayment | true → registers as checkout payment method |
$paymentMethodId | Value stored in orders.payment_method |
$paysBeforeOrder | true → payment before order creation (virtual POS) |
renderFrontTemplate — Important Rule
Always use $this->renderFrontTemplate('hook_name', $vars) instead of assigning variables
directly to global Smarty. This renders in an isolated sub-template and does not
pollute global variables like $isLoggedIn or $cart.
- Template path:
assets/templates/front/{name}.tpl - Returns HTML string — output in theme via
{$hooks.footer nofilter}
Admin Configuration
- URL:
/admin/module-{name}(not/admin/module?name=...) - Template:
assets/templates/admin/admin.tpl - All POST forms must validate
$adminTokenwithhash_equals()
API Endpoints
- URL:
POST /api/module.php?m={name}&action={action} - Storefront APIs must validate
$_SESSION['csrf_token']— use global JScsrfTokenfromheader.tpl - Never use admin token or cron token as CSRF token in storefront AJAX
- Direct access to
/modules/{name}/api/*.phpis blocked (403) — always route throughapi/module.php - Cron jobs: register as
'cron' => 'api/cron.php'and use/api/module.php?m={name}&action=cron&token=...
After Installation
- Create
modules/{name}/folder with required files - Go to Admin → Modules — module appears in list
- Click Install —
defaultDisplayHooksare assigned automatically - Adjust hook assignments on module detail page if needed
- Module is active by default after install
Security Checklist
- File guard:
if (!defined('IN_SCRIPT') && !defined('IN_ADMIN')) exit; - Admin forms:
hash_equals($adminToken, $postToken) - Store API: validate
csrfTokenfrom session - SQL: always use prepared statements via
DB::execute($sql, [$param]) - User input:
trim,strip_tags, Smarty|escape - No inline
<script>in hook templates — useassets/js/+data-api-url - Do not modify theme or core files
Reference Modules
| Module | Learn about |
|---|---|
whatsapp | Simple footer hook + SQL |
newsletter | API + footer form + CSRF pattern |
reviews | Product tabs + API + admin listing |
question | Product tabs + login-required API + anti-bot |
slider | Admin CRUD + image upload |
bankwire | Payment + order confirmation hook |
sanalpos | paysBeforeOrder + front route |
alert-price | Product hook + API + cron endpoint |
PHP 7.4 compatibility: Do not use
match, str_starts_with(),
named arguments, or union types. Use switch and strpos() instead.
PHP
<?php
if (!defined('IN_SCRIPT') && !defined('IN_ADMIN')) {
exit;
}
require_once dirname(__DIR__, 2) . '/core/ModuleBase.php';
class MyModuleModule extends ModuleBase
{
// ── Identity (REQUIRED) ───────────────────────────────
public string $name = 'my-module'; // must match folder name
public string $title = 'My Module';
public string $version = '1.0.0';
public string $description = 'Short description';
public string $author = 'FriSay';
// ── Display hooks ─────────────────────────────────────
public array $displayHooks = [
'footer' => 'Adds HTML to the footer area',
];
public array $defaultDisplayHooks = ['footer'];
// ── CSS/JS (empty array = load all files in assets/css or assets/js) ─
public array $frontStylesheets = ['front.css'];
public array $frontScripts = [];
public array $adminStylesheets = ['admin.css'];
public array $adminScripts = [];
// ── API endpoints ─────────────────────────────────────
public array $apiActions = [
'save' => 'api/save.php',
];
// ── Custom store routes: site.com/{slug} ────────────
public array $routes = [
// 'my-page' => 'front/page.php',
];
// ── REQUIRED: install / uninstall ─────────────────────
public function install(): bool
{
return $this->runSqlFile('install.sql');
// No database table: return true;
}
public function uninstall(): bool
{
return $this->runSqlFile('uninstall.sql');
}
// ── Optional: runs when module is loaded ──────────────
public function boot(): void
{
Module::registerHook('order.placed', function (array $order): void {
// Run after order is created
});
}
// ── Admin configure page (/admin/module-my-module) ────
public function adminPage(): void
{
global $smarty, $adminToken;
$flash = '';
if (Tools::isSubmit('saveMyModule')) {
$postToken = (string) Tools::getValue('token');
if (hash_equals($adminToken, $postToken)) {
Settings::set('MY_MODULE_SETTING', trim((string) Tools::getValue('setting')));
$flash = 'Saved';
} else {
$flash = 'Invalid request';
}
}
$smarty->assign([
'setting' => Settings::get('MY_MODULE_SETTING'),
'flash' => $flash,
]);
}
// ── Storefront hook output ────────────────────────────
public function renderDisplayHook(string $hook, array $context = []): ?string
{
if ($hook !== 'footer') {
return null;
}
$html = $this->renderFrontTemplate('footer', [
'message' => Settings::get('MY_MODULE_SETTING'),
]);
return $html !== '' ? $html : null;
}
}
admin.tpl (Smarty)
{* assets/templates/admin/admin.tpl *}
{if $flash}
<div class="alert alert-info">{$flash|escape}</div>
{/if}
<form method="post" class="admin-panel p-3" style="max-width:400px">
<input type="hidden" name="saveMyModule" value="1">
<input type="hidden" name="token" value="{$adminToken}">
<label class="form-label">Setting value</label>
<input type="text" name="setting" class="form-control" value="{$setting|escape}">
<button type="submit" class="btn btn-dark mt-3">Save</button>
</form>
api/save.php — Storefront API pattern
<?php
if (!defined('IN_SCRIPT')) {
exit;
}
header('Content-Type: application/json; charset=utf-8');
if ($_SERVER['REQUEST_METHOD'] !== 'POST') {
http_response_code(405);
echo json_encode(['success' => false, 'message' => 'Method not allowed']);
exit;
}
// Store CSRF — use global csrfToken in JS, NOT admin/cron tokens
$token = Tools::getValue('token') ?: ($_SERVER['HTTP_X_CSRF_TOKEN'] ?? '');
if (!hash_equals($_SESSION['csrf_token'] ?? '', (string) $token)) {
http_response_code(403);
echo json_encode(['success' => false, 'message' => 'Invalid request']);
exit;
}
$result = MyModuleModule::doSomething((string) Tools::getValue('field'));
echo json_encode($result);
front.js — AJAX call from storefront
// csrfToken is defined in header.tpl — do not hardcode tokens in templates
$(document).on('submit', '.my-module-form', function (e) {
e.preventDefault();
var $form = $(this);
$.ajax({
url: $form.data('api-url'),
method: 'POST',
dataType: 'json',
data: {
token: typeof csrfToken !== 'undefined' ? csrfToken : '',
field: $.trim($form.find('[name="field"]').val())
}
});
});
Payment module (bank transfer type)
<?php
public bool $isPayment = true;
public string $paymentMethodId = 'bank_transfer';
public string $paymentMethodLabel = 'Bank Transfer';
public bool $paysBeforeOrder = false;
// order_payment.tpl — radio value must match $paymentMethodId
public function processPayment(array $order): array
{
return [
'success' => true,
'redirect' => '',
'message' => '',
];
}
AI prompt template — paste into ChatGPT / Claude / Cursor
FriSay e-commerce project — write a module.
Rules: follow ALL rules in MODULE_DEVELOPER_GUIDE.md
PHP 7.4 compatible. Extend ModuleBase. Do NOT modify theme or core files.
Module info:
- name (slug): {my-module}
- title: {Display Name}
- description: {what it does}
Features:
- Display hooks: {footer, product_tab, ...}
- API actions: {action => description}
- Payment module: {yes/no}
- Database tables: {schema if any}
- Admin settings: {Settings keys or table fields}
Files to generate:
1. modules/{name}/{name}.php
2. install.sql / uninstall.sql (if needed)
3. assets/templates/admin/admin.tpl
4. assets/templates/front/{hook}.tpl (one per hook)
5. assets/css/, assets/js/ (if needed)
6. api/{action}.php (if needed)
7. front/{page}.php (if route needed)
Reference modules: whatsapp, newsletter, reviews, question, bankwire, sanalpos
Helper classes — common usage
<?php
// Settings (use UPPERCASE keys with module prefix)
Settings::get('DOMAIN'); // site URL — NOT 'domain'
Settings::get('SITE_NAME');
Settings::set('MY_MODULE_KEY', 'value');
// Database
DB::getRowSafe('table', 'id = ?', [$id]);
DB::insert('table', ['col' => 'value']);
DB::update('table', ['col' => 'v'], 'id = :where_id', ['where_id' => $id]);
DB::execute('DELETE FROM table WHERE id = ?', [$id]);
// Store data
$product = Product::getById($idProduct); // includes 'url' field
$images = Product::getImages($idProduct);
$cart = Cart::getSummary();
$isFav = Favorite::isFavorite($idProduct);
// Customer
Customer::isLoggedIn();
Customer::getId();
Customer::getCurrent();
// Tools
Tools::displayPrice(99.90); // ₺99,90
Tools::createSlug('ana sayfa'); // ana-sayfa
Tools::maskName('John Smith'); // J** S****
// Admin URL
Admin::url('module-my-module'); // https://site.com/admin/module-my-module
// Pagination
$pagination = Pagination::build($total, $page, 30, Admin::url('module-my-module'));
// Mail
Mail::send($to, $subject, $bodyHtml);