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.

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)

ItemRuleExample
Folder nameLowercase, hyphens allowedmy-module
Main file{name}/{name}.phpmodules/my-module/my-module.php
PHP classPascalCase + Modulemy-moduleMyModuleModule
Admin URLAuto-generated/admin/module-my-module
API URLVia module router/api/module.php?m=my-module&action=save
$name propertyMust match folder name exactlypublic string $name = 'my-module';

ModuleBase — Key Properties

PropertyPurpose
$displayHooksSupported hooks: hook_name => description
$defaultDisplayHooksHooks auto-assigned on install()
$apiActionsaction => api/file.php map
$routesslug => front/page.php custom store pages
$frontStylesheets / $frontScriptsAssets loaded on storefront (empty = load all files in folder)
$isPaymenttrue → registers as checkout payment method
$paymentMethodIdValue stored in orders.payment_method
$paysBeforeOrdertrue → 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 $adminToken with hash_equals()

API Endpoints

  • URL: POST /api/module.php?m={name}&action={action}
  • Storefront APIs must validate $_SESSION['csrf_token'] — use global JS csrfToken from header.tpl
  • Never use admin token or cron token as CSRF token in storefront AJAX
  • Direct access to /modules/{name}/api/*.php is blocked (403) — always route through api/module.php
  • Cron jobs: register as 'cron' => 'api/cron.php' and use /api/module.php?m={name}&action=cron&token=...

After Installation

  1. Create modules/{name}/ folder with required files
  2. Go to Admin → Modules — module appears in list
  3. Click InstalldefaultDisplayHooks are assigned automatically
  4. Adjust hook assignments on module detail page if needed
  5. 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 csrfToken from session
  • SQL: always use prepared statements via DB::execute($sql, [$param])
  • User input: trim, strip_tags, Smarty |escape
  • No inline <script> in hook templates — use assets/js/ + data-api-url
  • Do not modify theme or core files

Reference Modules

ModuleLearn about
whatsappSimple footer hook + SQL
newsletterAPI + footer form + CSRF pattern
reviewsProduct tabs + API + admin listing
questionProduct tabs + login-required API + anti-bot
sliderAdmin CRUD + image upload
bankwirePayment + order confirmation hook
sanalpospaysBeforeOrder + front route
alert-priceProduct 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);