Skip to content
Docs

Developer guide

Building modules

A module is the basic unit for adding features to Zittme. Your own DB tables, screens, admin settings, and even triggers that hook into other modules are all built as modules.

Folder structure

We recommend the modern, namespace-based structure.

modules/mymodule/
├── conf/
│   ├── info.xml        ← 이름·설명·버전·제작자
│   └── module.xml      ← 액션·권한·라우트·이벤트 핸들러
├── controllers/
│   ├── Base.php        ← 공통 상수·헬퍼 (ModuleObject 상속)
│   ├── Index.php       ← 화면 그룹별 컨트롤러 (목록)
│   ├── Read.php        ← (읽기)
│   ├── Write.php       ← (쓰기 화면 + proc)
│   ├── Admin.php       ← 관리자 화면·처리
│   ├── EventHandlers.php ← 트리거 핸들러
│   └── Install.php     ← 설치·업데이트 훅
├── models/             ← 데이터 로직
├── queries/            ← 쿼리 XML
├── schemas/            ← 테이블 XML
├── skins/default/      ← 사용자 화면 템플릿
├── views/admin/        ← 관리자 템플릿 (*.blade.php)
├── lang/ko.php         ← 언어 파일
└── composer.json       ← 컴포저 의존성을 쓸 경우

Classes use a namespace that matches the folder structure, such as Zittme\Modules\Mymodule\Controllers\Index.

  • The standard skeleton is to split controllers by screen group (Index/Read/Write...) rather than piling everything into one. In module.xml, class="Controllers\Read" assigns the responsible class to each action.
  • Modules that use Composer packages also declare the "require": { "rhymix/composer-stub": "dev-master" } stub in composer.json. This lets each module use its own vendor folder without conflicting with the core.
There is a module generator that produces exactly this structure: the Rhymix/XE module generator at poesis.dev. We recommend starting from the generator's output rather than from an empty skeleton.

module.xml: declaring actions

Only actions declared in module.xml can run. Calling an undeclared action returns 403 or 404.

<?xml version="1.0" encoding="UTF-8"?>
<module>
	<actions>
		<action name="dispMymoduleList" class="Controllers\Front" index="true">
			<route route="$page:int" priority="10" />
		</action>
		<action name="procMymoduleInsert" class="Controllers\Front" permission="member" />
		<action name="dispMymoduleAdminConfig" class="Controllers\Admin" permission="manager" admin-index="true" menu-name="mymodule" menu-index="true" />
		<action name="procMymoduleApiPing" class="Controllers\Api" standalone="true" check-csrf="false" />
	</actions>
	<menus>
		<menu name="mymodule">
			<title xml:lang="ko">내 모듈</title>
		</menu>
	</menus>
</module>

Key rules:

  • disp is GET only, proc is POST only. Calling a proc through a link (GET) returns 405. Make anything users reach from a screen a disp, and use proc only to receive form submissions.
  • proc actions check the CSRF token automatically. Callbacks and APIs called by external servers need standalone="true" check-csrf="false". The attribute name uses a hyphen (check-csrf). If you write it with an underscore, it is ignored and the check stays on.
  • index="true" is the default screen when accessed by mid.
  • permission specifies member (logged in), manager (management permission), and so on.

Routes

Adding a <route> gives you a short address in the form mid/value.

  • Variables are constrained as $name:type: int (integer 0 or greater) float alpha (letters) alnum (letters + digits) hex word (letters + digits + underscore) any (anything except slashes) delete (removed from the URL)
  • If you omit the type, variables ending in _srl are treated as int and everything else as any.
  • Routes with a higher priority number are matched first.
  • getUrl() automatically picks the route that best matches the variables you pass to build a short address, and appends any remaining variables not in the route as a query string.
  • With global-route="true", the route matches directly at the site root without a mid (/search and so on). This risks conflicts, so use it only when truly needed.
  • An action with error-handlers="404" becomes that module's 404 handler.

After editing module.xml, run the module update in the admin screen so routes and event handlers take effect.

Controllers

<?php

namespace Zittme\Modules\Mymodule\Controllers;

class Index extends Base
{
	/**
	 * init()은 이 컨트롤러의 액션이 실행되기 전에 자동 호출됩니다.
	 * 스킨 경로처럼 액션마다 반복되는 준비를 여기서 한 번에 합니다.
	 */
	public function init()
	{
		// 관리자가 고른 스킨을 존중하고, 없으면 default
		$this->setTemplatePath($this->module_path . 'skins/' . ($this->module_info->skin ?: 'default'));
	}

	public function dispMymoduleList()
	{
		$output = executeQueryArray('mymodule.getItems', new \stdClass);
		\Context::set('items', $output->data ?? []);

		$this->setTemplateFile('list');
	}

	public function procMymoduleInsert()
	{
		// Context::set이 같은 이름의 요청 변수를 덮어쓸 수 있으므로
		// proc에서는 $_POST를 직접 읽는 편이 안전합니다
		$title = trim((string)($_POST['title'] ?? ''));
		if ($title === '')
		{
			return new \BaseObject(-1, 'mymodule.msg_title_required');
		}

		$args = new \stdClass;
		$args->item_srl = getNextSequence();
		$args->title = $title;
		$args->regdate = date('YmdHis');

		$output = executeQuery('mymodule.insertItem', $args);
		if (!$output->toBool())
		{
			return $output;
		}

		$this->setMessage('success_registed');
		$this->setRedirectUrl(getNotEncodedUrl('', 'mid', \Context::get('mid')));
	}
}
  • Return errors with new \BaseObject(-1, 'langKey').
  • Language files are PHP files in the form $lang->msg_title_required = '...';.

Install and update: Install.php

The core creates tables from schemas/*.xml. The Install class handles any other initialization.

class Install extends Base
{
	public function moduleInstall()
	{
		return new \BaseObject();
	}

	// true를 돌려주면 관리자 화면에서 업데이트가 실행됩니다
	public function checkUpdate()
	{
		return false;
	}

	public function moduleUpdate()
	{
		return new \BaseObject();
	}

	public function recompileCache()
	{
	}
}

Important: The core does not automatically add new columns from the schema XML to tables that already exist. To add a column on a live site, write it in the schema XML and also add it in checkUpdate/moduleUpdate with $oDB->isColumnExists() / addColumn().

Triggers: hooking into other modules

The core and other modules call triggers in many places. Just declare an event handler in module.xml.

<eventHandlers>
	<eventHandler after="document.insertDocument" class="Controllers\Trigger" method="afterInsertDocument" />
</eventHandlers>

Write handlers defensively so an exception never takes down the core.

Common issues during development

  • New action returns 403/404: it isn't registered in module.xml, or the module update wasn't run
  • proc returns 405: it was called with GET. Change it to a disp or call it with a form POST
  • Values you put into a query disappear: columns not in the query XML's <columns> are silently dropped. See Query XML
  • Template 500: a multi-line {{-- --}} comment. See Template syntax v2