모듈 제작
모듈은 Zittme에서 기능을 추가하는 기본 단위입니다. 자체 DB 테이블, 화면, 관리자 설정, 다른 모듈에 끼어드는 트리거까지 모두 모듈로 만듭니다.
폴더 구조
네임스페이스 기반의 현대식 구조를 권장합니다.
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 ← 컴포저 의존성을 쓸 경우클래스는 Zittme\Modules\Mymodule\Controllers\Index 처럼 폴더 구조와 일치하는 네임스페이스를 씁니다.
- 컨트롤러는 하나로 몰지 말고 화면 그룹별로 나누는 것(Index/Read/Write...)이 표준 뼈대입니다. module.xml의
class="Controllers\Read"로 액션마다 담당 클래스를 지정합니다. - 컴포저 패키지를 쓰는 모듈은
composer.json에"require": { "rhymix/composer-stub": "dev-master" }스텁을 함께 선언합니다. 코어와 충돌 없이 모듈 단위 vendor 를 쓰게 해 줍니다.
이 구조 그대로 뽑아 주는 모듈 생성기가 있습니다: poesis.dev 의 라이믹스/XE 모듈 생성기. 빈 뼈대에서 시작하지 말고 생성기 결과물에서 시작하는 것을 권장합니다.
module.xml: 액션 선언
액션은 module.xml에 선언된 것만 실행됩니다. 선언하지 않은 액션을 호출하면 403 또는 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>핵심 규칙:
- disp는 GET, proc는 POST 전용입니다. proc를 링크(GET)로 호출하면 405가 납니다. 화면에서 진입해야 하는 동작은 disp로 만들고, 폼 제출만 proc로 받으세요.
- proc 액션은 CSRF 토큰을 자동 검사합니다. 외부 서버가 호출하는 콜백·API는
standalone="true" check-csrf="false"를 붙여야 합니다. 속성 이름은 하이픈(check-csrf)입니다. 밑줄로 쓰면 무시되어 검사가 켜진 채로 남습니다. index="true"는 mid로 접속했을 때의 기본 화면입니다.permission은member(로그인),manager(관리 권한) 등을 지정합니다.
라우트
<route> 를 붙이면 mid/값 형태의 짧은 주소가 됩니다.
- 변수는
$이름:타입으로 제한합니다:int(0 이상 정수)floatalpha(영문)alnum(영문+숫자)hexword(영문+숫자+밑줄)any(슬래시 제외 전부)delete(URL에서 제거) - 타입을 생략하면
_srl로 끝나는 변수는int, 나머지는any로 취급됩니다. priority숫자가 높은 라우트가 먼저 매칭됩니다.getUrl()은 넘긴 변수와 가장 잘 맞는 라우트를 자동으로 골라 짧은 주소를 만들고, 라우트에 없는 나머지 변수는 쿼리스트링으로 붙입니다.global-route="true"를 주면 mid 없이 사이트 루트에서 바로 매칭됩니다 (/search등). 충돌 위험이 있으니 꼭 필요할 때만 쓰세요.error-handlers="404"를 준 액션은 그 모듈의 404 처리기가 됩니다.
module.xml을 고친 뒤에는 관리자에서 모듈 업데이트를 실행해야 라우트·이벤트 핸들러가 반영됩니다.
컨트롤러
<?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')));
}
}- 오류는
new \BaseObject(-1, '언어키')로 돌려줍니다. - 언어 파일은
$lang->msg_title_required = '...';형식의 PHP 파일입니다.
설치와 업데이트: Install.php
테이블은 schemas/*.xml 을 보고 코어가 만듭니다. Install 클래스는 그 밖의 초기화를 담당합니다.
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()
{
}
}중요: 코어는 이미 만들어진 테이블에 스키마 XML의 새 칼럼을 자동으로 붙여 주지 않습니다. 운영 중 칼럼을 추가할 때는 스키마 XML에 적는 것과 함께 checkUpdate/moduleUpdate에서 $oDB->isColumnExists() / addColumn() 으로 붙여야 합니다.
트리거: 다른 모듈에 끼어들기
코어와 다른 모듈이 곳곳에서 트리거를 호출합니다. module.xml에 이벤트 핸들러를 선언하면 됩니다.
<eventHandlers>
<eventHandler after="document.insertDocument" class="Controllers\Trigger" method="afterInsertDocument" />
</eventHandlers>핸들러는 예외로 코어를 죽이지 않도록 방어적으로 작성하세요.