코드이그나이터(CI)의 기본라우터는 /system/Route.php 에 들어있는 CI_Router 클래스입니다.
이 클래스를 자세히 들여다보면 최종적으로 라우팅을 처리하는 메서드는 _validate_request 인데, 매우 제한적인 알고리즘으로 되어있습니다.
- 인자가 없다면 기본컨트롤러의 기본메서드에 연결한다
- 첫번째 인자가 폴더인 경우 두번째 인자를 해당 폴더의 컨트롤러 이름에, 세번째 인자부터 메서드에 연결한다
정도만 해줍니다. 엥 정말? 그렇습니다. 폴더 안의 폴더도 안해주고 폴더 안의 컨트롤러의 기본 메서드를 호출해주지도 않습니다(게다가 코드는 지저분하고 깁니다!)
이제 보다 개선된 라우팅을 만들어보죠.
확장하는 방법
라우터를 확장하는 방법은 사실상 한 가지만 제공됩니다. /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
- /controller/default.php 가 컨트롤러이고 action 이 메서드인 경우
- /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
이 경우를 생각해보면 어디까지가 서브폴더에 매칭되는지 루프를 통해 검사해봐야합니다. 따라서
- 폴더가 아닐 때까지 루프를 돌며 directory 를 갱신하고,
- 동시에 $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;
- 위의 로직을 이용하면 /test/action/ 폴더가 있고 run.php 가 있는 경우는 run 컨트롤러에 go메서드 로 알아서 처리되고,
- 반대로 /test/action/run/ 폴더가 있고 go.php 가 있는 경우는 go컨트롤러에 기본메서드 로 처리됩니다.
해당되는 컨트롤러가 존재하지 않는 경우의 처리
기본 라우터는 일단 인자를 계산하기 시작하면 무조건 해당 이름의 컨트롤러가 존재해야 하는데,
보강하여 컨트롤러가 없으면 그 폴더의 기본컨트롤러를 찾아서 메서드로 매칭해주는 식으로 확장해보겠습니다.
http://www.some.com/index.php/test/action/run/go
만약 /controller/test/ 폴더까지는 존재하지만, 그 안에 action.php 는 없고 오직 default.php 만 있는 경우를 생각해보죠. 그럼 이 경우
- 컨트롤러는 /controller/test/default.php 가 되고
- action 이 default 컨트롤러의 메서드가 되며
- [‘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 등의 메서드를 부를 생각이라면 필수적으로 해야합니다(위 예제에서는 중요로직 이해를 위해 생략했습니다)
recent comment