개발자 가이드

모듈 제작

모듈은 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 토큰을 자동 검사합니다. 외부 서버가 호출하는 콜백·APIstandalone="true" check-csrf="false" 를 붙여야 합니다. 속성 이름은 하이픈(check-csrf)입니다. 밑줄로 쓰면 무시되어 검사가 켜진 채로 남습니다.
  • index="true" 는 mid로 접속했을 때의 기본 화면입니다.
  • permissionmember(로그인), manager(관리 권한) 등을 지정합니다.

라우트

<route> 를 붙이면 mid/값 형태의 짧은 주소가 됩니다.

  • 변수는 $이름:타입 으로 제한합니다: int(0 이상 정수) float alpha(영문) alnum(영문+숫자) hex word(영문+숫자+밑줄) 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>

핸들러는 예외로 코어를 죽이지 않도록 방어적으로 작성하세요.

개발 중 자주 겪는 일

  • 새 액션이 403/404: module.xml 미등록이거나 모듈 업데이트를 안 돌린 경우
  • proc가 405: GET으로 호출한 경우. disp로 바꾸거나 폼 POST로 호출
  • 쿼리 결과에 넣은 값이 사라짐: 쿼리 XML의 <columns> 에 없는 칼럼은 조용히 버려집니다. 쿼리 XML 참고
  • 템플릿 500: 여러 줄 {{-- --}} 주석. 템플릿 문법 v2 참고