Part of the EllisLab Network
   
 
Dynamic Pages
Posted: 23 July 2008 05:28 PM   [ Ignore ]  
Summer Student
Total Posts:  11
Joined  05-23-2008

I am going to be building a website where there will be several pages that will use normal flat-files in the /controllers and /views directories, but I also want to be able to create custom pages through a simple user interface where the content will be stored in a database. The page URLs need to be fully customizable.

What I was thinking of doing is hooking somewhere around the place where it wants to fetch the 404 page, and making it check the database to see if the specified URI is in there (there would be a column in the database named uri with content like ’/custom/page’), and then serve the content if it’s there instead of the 404 page.

Is this the best solution or is there another (better?) way to do it? I don’t want to create new lines in the .htaccess file for every page created.

Profile
 
 
Posted: 23 July 2008 11:13 PM   [ Ignore ]   [ # 1 ]  
Research Assistant
RankRankRank
Total Posts:  788
Joined  06-19-2007

The best way to understand this is to answer your own question.  When index.php is opened it sets up some constants and path variables and then calls /codeigniter/codeigniter.php.

You need to open that file and go through it line-by-line until you understand the process that you think you just described in your post above.  When you understand what that one file does, you’ll have answered your own question.

If you want it the easy way - someone to answer the question for you—hang out, I’m sure someone will be along that isn’t as mean as me.

(You’ll be coming back anyway because the answer all by itself just isn’t enough information to get it done.)

Randy

p.s.  Have you thought through the SEO, DB query hassles, the URL helper, etc. implications of “fully customizable URLs”.  If not, you may want to read up on those things first.

 Signature 

My new therapist is working with me every day, the third one gave up… ohh

Profile
 
 
Posted: 24 July 2008 01:21 PM   [ Ignore ]   [ # 2 ]  
Summer Student
Total Posts:  11
Joined  05-23-2008

Thanks for the advice Randy. I went through the CodeIgniter.php file and all the files it includes line by line and I understand what’s going on a lot better now. I made a quick application in CI to see what I could do and this is what I came up with:

controllers/_cms.php

<?php

class _cms extends Controller
{
    
function __construct()
    
{
        parent
::Controller();
        
$this->load->model('cms_model');
    
}
    
    
function index()
    
{
        $page
= implode('/', $this->uri->segment_array());
        
        
$query = $this->cms_model->getPage($page);
        
        if (
$query->num_rows() > 0)
        
{
            $content
= $query->row();
            
            
$data['test'] = $content->content;
            
$this->load->view('cms_template', $data);
        
}
        
else
        
{
            show_404
($page . ' [CMS]');
        
}
    }
}

models/cms_model.php

<?php

class cms_model extends Model
{
    
function __construct()
    
{
        parent
::Model();
    
}
    
    
function getPage($path)
    
{
        $this
->db->where('uri', $path);
        return
$this->db->get('pages', 1);
    
}
}

The database is a table called “pages” with 2 columns (for now): uri and content.

I also edited the _validate_request() function in /system/libraries/Router.php and added:

// Can't find the requested controller...
# addition
        
$segments[0] = '_cms';
        
$segments[1] = 'index';
        
        
$this->set_class($segments[0]);
        
$this->set_method($segments[1]);
        
        return
$segments;
# /addition

(I also added it in the case that the controller lies in the subdirectory)

This works with almost every case except when the controller class exists, but the method doesn’t (ex: welcome/index will load the default CI page, but welcome/thisisdynamic will give a 404 despite being in the database).

I think I will have to include the class file in the _validate_request() function and do a check using get_class_methods(). Am I on the right track?

Profile
 
 
Posted: 24 July 2008 01:39 PM   [ Ignore ]   [ # 3 ]  
Research Assistant
RankRankRank
Total Posts:  788
Joined  06-19-2007

1) Since your application will pull this stuff from the DB by default, you may consider ‘auto loading’ your model.  You can do that from the autoload config file.  Just cleans the controller code a little.

2) You controller and model both look great.

3) Please talk me through your change (hack—sorry) to the router.php file.  Let’s see if we can get this done with CI’s native routing capabilities.  We should be able to use wildcard or regular expression routing in order to meet your needs to solve this for you.

You’re definitely on the right track.

Randy

 Signature 

My new therapist is working with me every day, the third one gave up… ohh

Profile
 
 
Posted: 24 July 2008 02:31 PM   [ Ignore ]   [ # 4 ]  
Summer Student
Total Posts:  11
Joined  05-23-2008

Here is the entire _validate_request() function as in the Routes.php file:

function _validate_request($segments)
    
{
        
// Does the requested controller exist in the root folder?
        
if (file_exists(APPPATH.'controllers/'.$segments[0].EXT))
        
{
            
return $segments;
        
}

        
// Is the controller in a sub-folder?
        
if (is_dir(APPPATH.'controllers/'.$segments[0]))
        
{        
            
// Set the directory and remove it from the segment array
            
$this->set_directory($segments[0]);
            
$segments = array_slice($segments, 1);
            
            if (
count($segments) > 0)
            
{
                
// Does the requested controller exist in the sub-folder?
                
if ( ! file_exists(APPPATH.'controllers/'.$this->fetch_directory().$segments[0].EXT))
                
{
                    
//log_message('debug', "Router Line 207");
####################################################################################################
                    
$segments[0] = '_cms';
                    
$segments[1] = 'index';
                    
                    
$this->set_class($segments[0]);
                    
$this->set_method($segments[1]);
                    
                    return
$segments;
####################################################################################################
                    
show_404($this->fetch_directory().$segments[0]);
                
}
            }
            
else
            
{
                $this
->set_class($this->default_controller);
                
$this->set_method('index');
            
                
// Does the default controller exist in the sub-folder?
                
if ( ! file_exists(APPPATH.'controllers/'.$this->fetch_directory().$this->default_controller.EXT))
                
{
                    $this
->directory = '';
                    return array();
                
}
            
            }

            
return $segments;
        
}
        
// Can't find the requested controller...
        
####################################################################################################
        
$segments[0] = '_cms';
        
$segments[1] = 'index';
        
        
$this->set_class($segments[0]);
        
$this->set_method($segments[1]);
        
        return
$segments;
####################################################################################################
        
show_404($segments[0]);
    
}

As you can see, my hack isn’t very pretty at the moment, mainly because I’m more concerned right now with making it work, rather than making it look nice.

At this point in the code, it’s do-or-die for CI: if the controller file doesn’t exist, the function calls the show_404() function which ends everything. The only hook that could be used is the pre_system one, which is called before the router file is included.

In essence, if the function checks that the controller file doesn’t exist, I replace the show_404() call with setting the class and method to my CMS page-loading class, and return the segments as if it found the right page for the URL. Then my CMS class checks the database for a URI match, and if it finds one, then it does whatever it needs to do, and otherwise it resorts to the show_404() call.

I hope that makes (enough) sense.

Profile
 
 
Posted: 24 July 2008 02:41 PM   [ Ignore ]   [ # 5 ]  
Research Assistant
RankRankRank
Total Posts:  788
Joined  06-19-2007

ok..right…that’s what I thought you were doing.  None of this is necessary. (oh - and remember, I’m the one that recommended you read all this stuff - so you can assume I’ve done that to wink )

You can set a default route.  What that means is you can make CI send everything to a default controller. That’s what I thought you intent was all along.  That allows you to override all this without the hack pretty much with your controller the way it is written right now.

Get into your routes config file and add this line…

$route[’:any’]=“_cms/load_page”;

then change your index() method name to load_page (or whatever) and create and empty index() method.

then—remove your hack from the router.php code !

Run it and request a non-existent controller.  I think you’ll be pleased.

Randy

 Signature 

My new therapist is working with me every day, the third one gave up… ohh

Profile
 
 
Posted: 24 July 2008 02:47 PM   [ Ignore ]   [ # 6 ]  
Summer Student
Total Posts:  11
Joined  05-23-2008

Thanks but I don’t think this is exactly what I am looking for. I haven’t tested your exact code, but from my past experiences with re-routes, I am assuming that it will redirect ALL requests to my CMS controller, regardless of whether there actually is a real controller (like in the case of /welcome/index) that should be called.

I want CI to first try and load the controller, and only if it doesn’t exist, pull the stuff from the database. Let’s say I have a welcome.php controller. If I go to mysite.com/welcome/index, I want it to load the welcome.php controller. However if I go to mysite.com/welcome/index2 or mysite.com/dynamic/information (and the “dynamic” controller doesn’t exist), it will pull the stuff from the database using the CMS controller.

Profile
 
 
Posted: 24 July 2008 03:19 PM   [ Ignore ]   [ # 7 ]  
Research Assistant
RankRankRank
Total Posts:  788
Joined  06-19-2007

yes, then…you are correct.  See, reading the file worked!  grin

OK…then you need to write a Hooks class and put your index method in to that.

So…

Get into your hooks config file and add this…

$hook[‘pre_controller’] = array(
        ‘class’  => ‘NoControllerPreempt’,
        ‘function’ => ‘Preempt’,
        ‘filename’ => ‘preempt.php’,
        ‘filepath’ => ‘hooks’,
        ‘params’  => array()
);

create a file named preempt.php in the hooks directory

the preempt.php file should contain the class NoControllerPreempt (or whatever)

it will need to…

if ( ! file_exists(APPPATH.'controllers/'.$RTR->fetch_directory().$RTR->fetch_class().EXT))  

yada yada

That class should have one method—your old index method from your controller

then—remove your hack from the router.php code !

Run it and request a non-existent controller.  I think you’ll be pleased.

So let’s try that then…but remove your hack !  wink

 Signature 

My new therapist is working with me every day, the third one gave up… ohh

Profile
 
 
Posted: 24 July 2008 04:18 PM   [ Ignore ]   [ # 8 ]  
Summer Student
Total Posts:  11
Joined  05-23-2008

I did what you suggested and I am sad to say that it doesn’t work. The pre_controller hook is being called too late (the 404 shows up prior to it). I changed it to a pre_system hook, but of course the Router class hasn’t been loaded yet. The unfortunate news is that loading the Router class also loads the URI class which, upon loading calls its _set_routing() method (which calls the ... which calls the _validate_request() method) ultimately leading us to another 404 error in the _validate_request() method, prior to my code being able to be executed:

hooks/preempt.php

<?php

class NoControllerPreempt
{
    
function Preempt()
    
{
        log_message
('debug', "In Preempt Hook");
        
        
$RTR =& load_class('Router');
        
        
print_r($RTR);
        
log_message('debug', "After Router Class Loaded"); // not logged if controller doesn't exist
        //echo APPPATH . 'controllers/' . $RTR->fetch_directory() . $RTR->fetch_class() . EXT;
        
        
if (!file_exists(APPPATH . 'controllers/' . $RTR->fetch_directory() . $RTR->fetch_class() . EXT))
        
{
           
die("yay");
        
}
        
else
        
{
            
die("nay");
        
}
    }
}

config/hooks.php

$hook['pre_system'] = array(
    
'class' => 'NoControllerPreempt',
    
'function' => 'Preempt',
    
'filename' => 'preempt.php',
    
'filepath' => 'hooks',
    
'params' => array()
);

If the class exists, then everything goes well (it dies outputting nay, or if I remove that it loads the controller normally). However it will never actually get to outputting yay because of the circular dependencies.

I don’t know if I’m jumping to conclusions yet, but I think I’ll need to hack the actual CI code. What do you think?

Profile
 
 
Posted: 24 July 2008 06:04 PM   [ Ignore ]   [ # 9 ]  
Research Assistant
RankRankRank
Total Posts:  788
Joined  06-19-2007

hi,

Right…you cannot load up the router class.  That is what blows out your 404 problem.

class NoControllerPreempt
{
    
function Preempt()
    
{
        
        $URI
=& load_class('URI');
        
$URI->_explode_segments();
        echo
'this is the path: '.APPPATH.'controllers/'.$URI->segments[1].EXT.'<br />';
        
        if ( !
file_exists(APPPATH.'controllers/'.$URI->segments[1].EXT))  
        
{
            
echo ' Page does not exist';
        
}
        
else
        
{
            
echo ' Page exists';
        
}
    
exit;
    
}
}


Here is a very simplistic replacement class that works perfectly well. I’ve tested this and it works as expected.

you’ll have a lot of work to do with the path parsing and all that but later, but this simple example will demonstrate how to make this all work.

Randy

 Signature 

My new therapist is working with me every day, the third one gave up… ohh

Profile
 
 
Posted: 25 July 2008 04:33 PM   [ Ignore ]   [ # 10 ]  
Summer Student
Total Posts:  11
Joined  05-23-2008

Hi Randy,

Thanks for all the help you’ve given me so far. I’ve come up with a solution that works me (that you may disagree with), and I’ll do some testing on it to make sure there are no bugs. Here is what I did:

application/config/autoload.php

//...
$autoload['model'] = array('cms_model');
//...

application/controllers/_cms.php

<?php

class _cms extends Controller
{
    
function __construct()
    
{
        parent
::Controller();
    
}
    
    
function load_page()
    
{
        $page
= implode('/', $this->uri->segment_array());
        
        
$query = $this->cms_model->getPage($page);
        
        if (
$query->num_rows() > 0)
        
{
            $page
= $query->row();
            
            
$data['content'] = $page->content;
            
$this->load->view('cms_template', $data);
        
}
        
else
        
{
            show_404
($page . ' [CMS]');
        
}
    }
}

application/models/cms_model.php

<?php

class cms_model extends Model
{
    
function __construct()
    
{
        parent
::Model();
    
}
    
    
function getPage($path)
    
{
        $this
->db->where('uri', $path);
        return
$this->db->get('pages', 1);
    
}
}

application/views/cms_template.php

<h1>CMS Sample Page</h1>
<
p><?=$content;?></p>

application/libraries/MY_Router.php

<?php

class MY_Router extends CI_Router
{
    
function __construct()
    
{
        parent
::CI_Router();
    
}
    
    
function _validate_request($segments)
    
{
        
// Does the requested controller exist in the root folder?
        
if (file_exists(APPPATH.'controllers/'.$segments[0].EXT))
        
{
            
if (isset($segments[1]) && $segments[1] != 'index')
            
{
                $data
= file_get_contents(APPPATH.'controllers/'.$segments[0].EXT);
                if (!
preg_match("/\bfunction\w+" . $segments[1] . "\(/i", $data))
                
{
                    
return $this->cmsHook();
                
}
            }
            
            
return $segments;
        
}

        
// Is the controller in a sub-folder?
        
if (is_dir(APPPATH.'controllers/'.$segments[0]))
        
{
            
// Set the directory and remove it from the segment array
            
$this->set_directory($segments[0]);
            
$segments = array_slice($segments, 1);
            
            if (
count($segments) > 0)
            
{
                
// Does the requested controller exist in the sub-folder?
                
if ( ! file_exists(APPPATH.'controllers/'.$this->fetch_directory().$segments[0].EXT))
                
{
                    
return $this->cmsHook();
                    
//show_404($this->fetch_directory().$segments[0]);
                
}
            }
            
else
            
{
                $this
->set_class($this->default_controller);
                
$this->set_method('index');
            
                
// Does the default controller exist in the sub-folder?
                
if ( ! file_exists(APPPATH.'controllers/'.$this->fetch_directory().$this->default_controller.EXT))
                
{
                    $this
->directory = '';
                    return array();
                
}
            
            }

            
return $segments;
        
}
        
        
return $this->cmsHook();
        
        
// Can't find the requested controller...
        //show_404($segments[0]);
    
}
    
    
function cmsHook()
    
{
        $segments[0]
= '_cms';
        
$segments[1] = 'load_page';
        
        
$this->set_class($segments[0]);
        
$this->set_method($segments[1]);
        
        return
$segments;
    
}
}

I have also solved the problem where the controller exists but the method doesn’t (welcome/index vs. welcome/index2) by reading the controller file (if it exists) and using a simple regex match. This probably isn’t the most efficient, but for now, it is fast enough to be good enough.

I know you don’t like it when people hack the core files, but here is my reasoning behind it:

1. Using the pre_system hooks, I would have to essentially rewrite the whole controller loading process because it hasn’t been loaded yet. This would be redundant (and not very pragmatic wink).

2. I want to be able to use the templates in the /view directory for the CMS pages as well, so it would be most logical to have all those classes loaded too - might as well let the normal CI system load it up for me to prevent duplication and possible bugs.

What do you think?

Profile
 
 
Posted: 25 July 2008 09:27 PM   [ Ignore ]   [ # 11 ]  
Research Assistant
RankRankRank
Total Posts:  788
Joined  06-19-2007

Hi,

This is what I think…

If “I’ve come up with a solution that works me” is what you’ve done.  Then by all means press on with it. 

Along the way, you’ve violated all sorts of time tested rules that will make your life a little more difficult over the long term.  That will not kill you or your project, but will provide great learning points along the way I think.  So all is OK, but maybe not perfect.  For the record, I’ve never built a perfect project wink

So this is a good starting point and I’m glad I was able to get you set on a path you feel comfortable with. 

Randy

 Signature 

My new therapist is working with me every day, the third one gave up… ohh

Profile
 
 
Posted: 22 August 2008 07:36 PM   [ Ignore ]   [ # 12 ]  
Grad Student
Rank
Total Posts:  32
Joined  04-24-2008

Have you looked into _remap() ?

Profile
 
 
   
 
 
Post Marker Legend
New Topic New posts Hot Topic Hot Topic with new posts New Poll New Poll Moved Topic Moved Topic Sticky Topic Sticky topic
Old Topic No new posts Hot Old Topic Hot Topic with no new posts Old Poll Old Poll Closed Topic Closed Topic Announcement Announcements
Theme
Change Theme
Visitor Statistics
The most visitors ever was 719, on June 06, 2008 10:16 AM
Total Registered Members: 62604 Total Logged-in Users: 25
Total Topics: 77089 Total Anonymous Users: 1
Total Replies: 416305 Total Guests: 180
Total Posts: 493394    
Members ( View Memberlist )
Newest Members:  wengbaoshanGenki1gabewellsGlaucoeudj1nsehartEasyMLance SloanandrewjhscottShuvo