[CI] 라우터(CI_Router) 확장하기

코드이그나이터(CI)의 기본라우터는 /system/Route.php 에 들어있는 CI_Router 클래스입니다.

이 클래스를 자세히 들여다보면 최종적으로 라우팅을 처리하는 메서드는 _validate_request 인데, 매우 제한적인 알고리즘으로 되어있습니다.

  1. 인자가 없다면 기본컨트롤러의 기본메서드에 연결한다
  2. 첫번째 인자가 폴더인 경우 두번째 인자를 해당 폴더의 컨트롤러 이름에, 세번째 인자부터 메서드에 연결한다

정도만 해줍니다. 엥 정말? 그렇습니다. 폴더 안의 폴더도 안해주고 폴더 안의 컨트롤러의 기본 메서드를 호출해주지도 않습니다(게다가 코드는 지저분하고 깁니다!)

이제 보다 개선된 라우팅을 만들어보죠.

 
 

확장하는 방법

라우터를 확장하는 방법은 사실상 한 가지만 제공됩니다. /application/libraries/MY_Router.php 를 만드는거죠.

라이브러리에 CI_Router 를 상속받은 서브클래스를 만들면 접두어 규칙에 의해 CodeIgniter 가 알아서 불러오게 됩니다(정확하게는 /system/core/Loader.php_ci_load_class 메서드가 수행합니다)

간단히 이를 코드로 표현하면 다음과 같습니다.

/application/libraries/BS_Router.php

class BS_Router extends CI_Router{
	function _validate_request( $segment ){

		//사용자 정의 라우팅

	}
}

(하지만 모든 요청에 발동되며 동시에 한 번만 쓰고 버려지는 라우터의 특성상 예외적으로 /system/core/Router.php 에 있는 메서드를 직접 수정하는 것도 개인적으로는 추천드립니다.

고작 컨트롤러 선택해서 불러주는게 임무의 끝인데 요청마다 한 번 쓰고 버려질 라우터따위에 cpu파워를 주기도 아까운…)

 
 

아무 인자도 안준 경우

이 경우의 URL은 다음과 같습니다.

http://www.some.com/index.php

이 URL을 보면 index.php 뒤에 아무것도 없으니 $segment 가 빈 배열이 와야 맞을 것 같습니다….만! 실은 기본컨트롤러에 기본메서드가 채워서 옵니다.
(기본컨트롤러는 /application/config/routes.php 에서 지정합니다)

$route['default_controller'] = 'default';

또한 기본 메서드는 따로 지정하지 않아도 CI_Router 의 기본값이 index 입니다. 따라서 이정도 설정이라면 위의 URL에서 기대할 수 있는 $segment 

['default', 'index']

가 됩니다.

다른 경우는 전부 인자를 파싱해서 들어오는지라 이건 매우 특수한 상황입니다. 따라서 별도로 분기할 필요가 있습니다.

function _validate_request( $segment ){

	// 첫번째인자가 기본컨트롤러고 두번째인자가 기본메서드라면
	if( $segment[0]==$this->default_controller && $segment[1]==$this->method ){
		//그대로 반환하면 끝!
		return $segment;


	//그외의 경우
	}else{

	}
}

이런 느낌입니다. 참고로 _validate_request 는 최종적으로 배열을 반환하게 되어있는데 배열의 형식은 다음과 같습니다.

[컨트롤러명, 메서드명, 인자1, 인자2, …]

 
 

인자가 컨트롤러폴더의 서브폴더에 매칭되는 경우

이 경우부터 까다로워지는데 다음과 같은 URL을 생각해보면 두가지 경우를 상정할 수 있습니다.

http://www.some.com/index.php/test/action

  1. /controller/default.php 가 컨트롤러이고 action 이 메서드인 경우
  2. /controller/test/action.php 가 컨트롤러이고 기본메서드인 index 로 호출되는 경우

이 경우의 판단은 컨트롤러에 test 라는 폴더가 존재하는가로 판단하면 됩니다.

//$segment는 현재 [ 'test', 'action'] 인 상태

//서브폴더라면
if( is_dir( APPPATH.'controllers/'.$segment[0] ) ){

	//디렉토리정보갱신
	$this->directory .= $segment[0] .'/';

	//첫번째인자 test는 폴더이름이니 제거 ['action']
	array_shift( $segment );

	//메서드가 없다면 기본 메서드를 추가 ['action', 'index']
	if( count($segment) === 1 ) array_push( $segment, $this->method );

	return $segment;
}

근데 만약 URL이 더욱 복잡해지면 이러한 판단은 재귀적으로 일어납니다.

http://www.some.com/index.php/test/action/run/go

이 경우를 생각해보면 어디까지가 서브폴더에 매칭되는지 루프를 통해 검사해봐야합니다. 따라서

  1. 폴더가 아닐 때까지 루프를 돌며 directory 를 갱신하고,
  2. 동시에 $segment 의 인자 카운팅을 통해 컨트롤러를 구별해야합니다.
//루프카운터
$i = 0;

//경로계산
$dir = '';

//컨트롤러루트
$root = APPPATH.'controllers/';

//인자를 하나씩 더하며 여전히 폴더라면
while( is_dir( $root.$dir.'/'.$segment[$i] ) ){

	//실제로 폴더경로를 갱신
	$dir .= '/'.$segment[$i++];

}

//경로를 갱신한다.
$this->directory .= $dir.'/';

//카운트 만큼 경로에 포함되므로 제거한다.
array_splice( $segment, 0, $i );

//메서드가 없다면 기본 메서드를 추가
if( count($segment) === 1 ) array_push( $segment, $this->method );

//남은 배열이 자동으로 [컨트롤러, 메서드, 인자...] 가 된다.
return $segment;
  1. 위의 로직을 이용하면 /test/action/ 폴더가 있고 run.php 가 있는 경우는 run 컨트롤러에 go메서드 로 알아서 처리되고,
  2. 반대로 /test/action/run/ 폴더가 있고 go.php 가 있는 경우는 go컨트롤러에 기본메서드 로 처리됩니다.

 
 

해당되는 컨트롤러가 존재하지 않는 경우의 처리

기본 라우터는 일단 인자를 계산하기 시작하면 무조건 해당 이름의 컨트롤러가 존재해야 하는데,
보강하여 컨트롤러가 없으면 그 폴더의 기본컨트롤러를 찾아서 메서드로 매칭해주는 식으로 확장해보겠습니다.

http://www.some.com/index.php/test/action/run/go

만약 /controller/test/ 폴더까지는 존재하지만, 그 안에 action.php 는 없고 오직 default.php 만 있는 경우를 생각해보죠. 그럼 이 경우

  1. 컨트롤러는 /controller/test/default.php 가 되고
  2. action default 컨트롤러의 메서드가 되며
  3. [‘run’, ‘go’]action 메서드의 인자가 되어야합니다.

위에서 작성한 로직을 조금만 손보면됩니다.

//상동
$i = 0;
$dir = '';
$root = APPPATH.'controllers/';
while( is_dir( $root.$dir.'/'.$segment[$i] ) ){
	$dir .= '/'.$segment[$i++];
}
$this->directory .= $dir.'/';
array_splice( $segment, 0, $i );

//물리경로
$path = APPPATH.'controllers'.$dir.'/';

//해당 경로에 그 이름의 라우터가 존재하는가?
if( file_exists( $path.$segment[0].EXT ) ){

	//기본메서드처리
	if( count($segment) === 1 ) array_push( $segment, $this->method );

	return $segment;

//아니면 기본컨트롤러는 존재하는가
}else if( file_exists( $path.$this->default_controller.EXT ) ){

	//기본메서드처리
	if( count($segment) === 0 ) array_push( $segment, $this->method );

	//중요! 컨트롤러를 기본컨트롤러로 젤 앞에 삽입
	array_unshift( $segment, $this->default_controller );

	return $segment;
}

폴더 확인을 끝내고 나서 인자상의 컨트롤러가 존재하는지 아니면 기본컨트롤러가 존재하는지 확인하여 각각에 맞게 $segment 를 조정해줍니다.

 
 

전체 코드 정리 및 404처리

이제 위의 코드를 다 모아서 하나의 완전한 서브클래스를 만들어보죠.

class BS_Router extends CI_Router{
	function _validate_request( $seg ){
		if( $seg[0] === $this->default_controller && $seg[1] === $this->method ){
			return $segment;
		}else{
			$i = 0;
			$dir = '';
			$root = APPPATH.'controllers/';
			while( is_dir( $root.$dir.'/'.$seg[$i] ) ){
				$dir .= '/'.$seg[$i++];
			}
			$this->directory .= $dir.'/';
			array_splice( $seg, 0, $i );
			$path = APPPATH.'controllers'.$dir.'/';
			if( file_exists( $path.$seg[0].EXT ) ){
				if( count($seg) === 1 ) array_push( $seg, $this->method );
				return $segment;
			}else if( file_exists( $path.$this->default_controller.EXT ) ){
				if( count($seg) === 0 ) array_push( $seg, $this->method );
				array_unshift( $seg, $this->default_controller );
				return $seg;
			}
		}
		show_404($seg[0]);
	}
}

 
 

결론

CI 의 제한적인 라우터기능을 범용적으로 확정해봤습니다.

사실 라우터를 확장하면 하나의 도메인에 다중 사이트를 만들거나 여러 도메인으로부터 들어온 경우를 하나의 CI에서 분리하여 처리하는 시스템이 가능합니다.
라우터의 기본적인 수정방법만 숙지하면 큰 어려움은 없습니다.

  • _validate_request 함수에 최초 $segment 가 어떤 식으로 주어지는가
  • _validate_request 함수는 반드시 두 가지는 처리해야 함.
  • 우선 $this->directory 를 서브폴더 처리에 맞게 갱신할 것
  • [컨트롤러명, 메서드명, 인자…] 형태인 배열을 반환할 것

이것만 잘 지키면 됩니다. 하지만 추가적으로는 두 가지를 더 해야합니다.

  • 현재 선택된 컨트롤러를 기록합니다 – $this->set_class(컨트롤러명);
  • 현재 선택된 메서드를 기록합니다 – $this->set_method(메서드명);

위 두개의 셋팅은 하위 컨트롤러나 기타 페이지에서 라우터의 fetch_class, fetch_method 등의 메서드를 부를 생각이라면 필수적으로 해야합니다(위 예제에서는 중요로직 이해를 위해 생략했습니다)

%d 블로거가 이것을 좋아합니다: