Soup to Nuts - The Complete Package

Soup to Nuts: Extending the User Module in PyroCMS

Soup to Nuts - The Complete Package

I’ve been helping get a new site off the ground that uses the PyroCMS system. If you haven’t heard of it, it is a CMS (content management system) that is built on top of the CodeIgniter PHP framework. Having recently gone back into PHP and starting fresh with the MVC approach to web development, I’ve been quite impressed with how easy I’ve been able to get things up and running.

That said, I ran into one hurdle with our new site. We needed students to register on the site and track some additional information about them. I did a little digging in the forums and online, and the general recommendation was not to override the user module built into the system, because future updates might overwrite your changes or cause some incompatibilities. The preferred solution is to create your own module to store your student related data, and tie into the user module as needed.

Since there is a lot of flexibility available with MVC, this approach was easy to implement. What makes things really nice is that we can use the existing user module built into the PyroCMS system, which allows us to simply strip out the duplicate references (no need to rewrite the user model) and work from there. I like this “learn from an existing” model approach to things, because you can see how the code is supposed to generally flow within the framework.

That said, I dug down to the public/system/pyrocms/modules/users folder and copied it to a separate location for me to work with. The first thing to do was to load up the details.php file in the root of the folder and modify the basic details. It also allows you to specify an install and uninstall method, which we can leverage to create the necessary tables we need:

public function install()


// Create student tables and add student category to user groups.

$sql = 'CREATE TABLE IF NOT EXISTS ' . $this->db->dbprefix('student') . '



user_id int NOT NULL,

age int NOT NULL,

grade_level int NOT NULL,

gender char NOT NULL,

esl char NOT NULL,

sport_id int NOT NULL,

sport_level_id int NOT NULL,

created datetime NOT NULL,

modified datetime NOT NULL,





$sql = 'CREATE TABLE IF NOT EXISTS ' . $this->db->dbprefix('student_secondary_sport') . '



student_id int NOT NULL,

sport_id int NOT NULL,

created datetime NOT NULL,

modified datetime NOT NULL,





$sql = 'INSERT INTO ' . $this->db->dbprefix('groups') . ' (name, description)

VALUES (\'student\', \'Students\')';


return TRUE;


public function uninstall()


// Remove student related data and module settings

$sql = 'DELETE FROM '. $this->db->dbprefix('groups') . ' WHERE name = \'student\'';


$sql = 'DROP TABLE '. $this->db->dbprefix('student_secondary_sport');


$sql = 'DROP TABLE '. $this->db->dbprefix('student');


$sql = 'DELETE FROM '. $this->db->dbprefix('modules') . ' WHERE name = \'student\'';


return TRUE;


Since we can call the appropriate user models and helpers, there’s no need to keep this code, so we remove a lot of files:

  • config/ion_auth.php
  • controllers/profile.php
  • language/* – leave the english folder, since you’ll want to update a few of your tags. Keep other languages if you plan on translate
  • libraries/*
  • models/profiles_m.php
  • models/ion_auth_model.php
  • flush most of your views, except for the register.php view and the admin, email, and settings subfolders

After removing the extra files, the next step is to go in and rename all of your user type files to student, or whatever you want to call them. You’ll want to go through the remaining files and weed out the methods that aren’t needed, such as login, since those are covered by the user model. After this, you need to inject the appropriate code into the appropriate methods. For instance, in the student controller, I need to retrieve all of the student related data from my view and insert that into the proper tables after a successful user insertion. The register method winds up looking like this:


* Method to register a new student

* @access public

* @return void


public function register()


// Validation rules

$validation = array(


'field' => 'first_name',

'label' => 'lang:user_first_name_label',

'rules' => 'required|utf8'



'field' => 'last_name',

'label' => 'lang:user_last_name_label',

'rules' => 'required|utf8'



'field' => 'email',

'label' => 'lang:user_email_label',

'rules' => 'required|valid_email'



'field' => 'password',

'label' => 'lang:user_password_label',

'rules' => 'min_length[6]|max_length[20]'



'field' => 'confirm_password',

'label' => 'lang:user_password_confirm_label',

'rules' => 'matches[password]'



'field' => 'username',

'label' => 'lang:user_username',

'rules' => 'required|alphanumeric|min_length[3]|max_length[20]'



'field' => 'display_name',

'label' => 'lang:user_display_name',

'rules' => 'alphanumeric|min_length[3]|max_length[50]'



'field' => 'active',

'label' => 'lang:user_active_label',

'rules' => ''



'field' => 'age',

'label' => 'lang:student_age',

'rules' => 'required|integer'



'field' => 'grade_level',

'label' => 'lang:student_grade_level',

'rules' => 'required|integer'



'field' => 'gender',

'label' => 'lang:student_gender',

'rules' => 'required|alpha'



'field' => 'esl',

'label' => 'lang:student_esl',

'rules' => 'required|alpha'



'field' => 'sport_id',

'label' => 'lang:student_sport',

'rules' => 'required|numeric'



// Set the validation rules


$email = $this->input->post('email');

$password = $this->input->post('password');

$username = $this->input->post('username');

$group_id = (int)$this->input->post('group_id');

$age = (int)$this->input->post('age');

$grade_level = (int)$this->input->post('grade_level');

$gender = $this->input->post('gender');

$esl = $this->input->post('esl');

$sport_id = (int)$this->input->post('sport_id');

$sport_level_id = (int)$this->input->post('sport_level_id');

$secondary_sport_ids = array();

$sports = get_sports();

$sportids = array_keys($sports);

foreach ($sportids as $id)


$secondaryid = 'secondary_sport_' . $id;



$secondary_sport_ids[] = $id;



$user_data_array = array(

'first_name' => $this->input->post('first_name'),

'last_name' => $this->input->post('last_name'),

'display_name' => $this->input->post('display_name'),

'group_id' => (int)$this->input->post('group_id'),

'age' => (int)$this->input->post('age'),

'grade_level' => (int)$this->input->post('grade_level'),

'gender' => $this->input->post('gender'),

'esl' => $this->input->post('esl'),

'sport_id' => (int)$this->input->post('sport_id'),

'sport_level_id' => (int)$this->input->post('sport_level_id'),

'secondary_sport_ids' => $secondary_sport_ids


// Convert the array to an object

$user_data = new stdClass();

$user_data->first_name = $user_data_array['first_name'];

$user_data->last_name = $user_data_array['last_name'];

$user_data->display_name = $user_data_array['display_name'];

$user_data->username = $username;

$user_data->email = $email;

$user_data->password = $password;

$user_data->confirm_email = $this->input->post('confirm_email');

$user_data->group_id = (int)$this->input->post('group_id');

$user_data->age = (int)$this->input->post('age');

$user_data->grade_level = (int)$this->input->post('grade_level');

$user_data->gender = $this->input->post('gender');

$user_data->esl = $this->input->post('esl');

$user_data->sport_id = (int)$this->input->post('sport_id');

$user_data->sport_level_id = (int)$this->input->post('sport_level_id');

$user_data->secondary_sport_ids = $secondary_sport_ids;

if ($this->form_validation->run())


$group = $this->group_m->get($this->input->post('group_id'));

// Try to create the user

if ($id = $this->ion_auth->register($username, $password, $email, $user_data_array, $group->name))


// Insert the student information to the proper table and add it to the user data object.

$sql = 'INSERT INTO student (user_id, age, grade_level, gender, esl, sport_id, sport_level_id, created, modified)

VALUES (' . $this->db->escape($id) . ', '

. $this->db->escape($age) . ', '

. $this->db->escape($grade_level) . ', '

. $this->db->escape($gender) . ', '

. $this->db->escape($esl) . ', '

. $this->db->escape($sport_id) . ', '

. $this->db->escape($sport_level_id) . ',





$student_id = $this->db->insert_id();

$user_data->student_id = $student_id;

// Insert any secondary sports for the student.

if (isset($secondary_sport_ids) && count($secondary_sport_ids > 0))


foreach ($secondary_sport_ids as $sportid)


$sql = 'INSERT INTO student_secondary_sport (student_id, sport_id, created, modified)

VALUES (' . $this->db->escape($student_id) . ', '

. $this->db->escape($sportid) . ',







$this->session->set_flashdata(array('notice' => $this->ion_auth->messages()));



// Can't create the user, show why



$this->data->error_string = $this->ion_auth->errors();





// Return the validation error

$this->data->error_string = $this->form_validation->error_string();


$this->data->user_data =& $user_data;


$this->template->build('register', $this->data);


Obviously there’s a lot more to process, but it’s a lot simpler than it looks. It’s just tricky to piece things together, at least if you’re not too familiar with CodeIgniter, PyroCMS, or MVC in general. However, I made it through it and you can too!

Once you get your module code complete, you can copy it into your addons/modules folder and then login to your site. In your Add-Ons section you should see where you can install the module, which will create your tables and get you going. PyroCMS makes it really nice this way to plug new stuff in to your site. Students can register using the proper path of /students/register, and standard users can still register using the path of /register or /users/register. In addition, with the latest 1.3 release of PyroCMS, there is an event model that has been updated. With the new event model, you could detect a user login, and then do some additional processing, such as grab the student information and add it to the $this->user object that is available in the system.

The next step for this, is to be able to abstract the module more, so that you could create custom fields for as many groups as you need. Then you could share these properties across multiple groups. For instance, you may have a property called “School” that would be needed by teachers and students, but only the “primary sport” property would be needed by the students.

I’ve gone ahead and attached the module code I’ve written, in hopes of helping you get off the ground with extending the user model in PyroCMS to meet your own needs. This code is functional on our site, but I’m sure there’s always a few tweaks or updates that would make things nicer. Please feel free to drop me a line if you see any of those.


Donwload the module from the git repository.


41 thoughts on “Soup to Nuts: Extending the User Module in PyroCMS”

  1. Hey Dillie-O,

    I feel your pain here. We know all about the inadequacies of the profile system at the moment and we’d love to have the time to sort it out. Profiles are the way they are because that is what a client wanted… three years ago… with basically no budget.

    A lot of the CMS used to be like that but since then interested individuals, contracted developers, funded companies, etc have taken it upon themselves for various reasons to improve the system and slowly everything has got considerably better.

    Profile is one are that everyone wants to be better but has just never felt the love! One day.

    1. Greetings Phil! I really appreciate the feedback, and I really appreciate all you’ve done with PyroCMS. I was able to get a site of the ground in two days, with some basic template pages and even made a widget later on to grab some data for a news feed, so I think that’s a great indicator of how well things are going.

      Keep up the hard work! You’ve got another follower in the product and hopefully I’ll have something to contribute. 8^D

  2. Hello Dilly-O,

    Nice… very nice… Like Phil says.. everyone extends the profile module. There are several ways.
    Looking at your code, please bare in mind you use the sql inside your model, not your controller. MVC concept requires you to separate these logics.

    In general.. nicely done… well readable.

    1. Thank you very much Henk! I’m still getting my feet wet with MVC, so I really appreciate the feed back and the code criticism. I’ll work on getting my SQL migrated out to the proper place. It’s a good exercise for me to get better with it. I’m already seeing the immense benefits of MVC, and I’ve only been about two months into it.

  3. Fantastic, thanks for blogging this Dillie-O. Am in the process of starting to modify user profiles and registration and looking at ways of going about it. Was originally going to alter standard tables and authentication files but was worried about future updates to DB and code so might go down your route of creating a new module for this and storing extra info in an additional table.

    What do you do when a user goes to visit their profile? Do you detect whether the user is a student and use your student module code to display a different profile if it is? My gut instinct is to want to create only one profile type but I see there are advantages to perhaps not going down this route

    1. Currently I have the “standard” routes of updating the profile inaccessible to the students. I created my own edit page to allow them to modify their student details and main account information.

      In the future I see this model expanding to more of a “user extended properties” type module. Then on the settings page, it would detect the type of user (as you mentioned) and pull together the properties needed to properly edit things.

      I’m glad I could be of some help! Let me know if there’s anything else I can mention to help;

      1. Thanks very much. I think this is the route I’m going to go down, extending the user properties with a new module. I’m also dealing with students, groups etc.

        Thanks for your help and sharing, look like you got up to speed with it quickly.


      2. Do you know which version of Pyro the github module is compatible with? I tried to install it but got complaints about various tables from the SQL, I think to do with when Pyro changed to multi site and prefixed some with ‘default_’. I renamed some of the tables to eliminate the errors then got a blank screen. Don’t have too much spare time to play with it but thought I’d ask the question about version compatibility.

  4. When I set it up it was for 1.2, about a week before 1.3 came out (I didn’t realize it at the time). I had to make my own modifications for 1.3, so I’ll go peek at it today and see if I can get that updated.

    Thanks for the catch!

    1. I found it. In the helpers/student_helper.php file, there are a couple of functions using Active Record (is_student and get_student_id) which don’t have the proper db prefix, if you take those queries and modify them to read

      FROM ‘ . ci()->db->dbprefix(‘student’) . ‘

      you should be set. I need to tweak the code (and figure out git updates really quick 8^D) but you should be okay doing that yourself.

      1. Does this apply to the SQL in students_m.php also? e.g. get_all() has no dbprefix on any table names, so is trying to get from users, groups etc instead of default_users, default_groups etc (assuming default_ is your prefix)

        Also getting a message about is_ajax() being undefined in the admin controller (ln141)

        Will try and work through errors as seeing it working would be useful

        1. Wow, I forgot all those little tweaks!

          As for the AJAX, the method changed with version 1.3, it should read:

          //unset the layout if we have an ajax request
          $this->input->is_ajax_request() ? $this->template->set_layout(FALSE) : ”;

          As for the get_all method, I was doing a bad job and jumping between “traditional” SQL and “active record” approaches to get the data.

          Within the model, I believe active record is auto pre-pending the default_ prefix on all of the queries, since I didn’t run into any issues. You’ll also notice that I overrode the default table name at the top by setting the $_table value to ‘student’. I’m a little “old school” in my table naming (no plurals) but I could see using the plurals in future updates to follow the conventions.

          Hope this helps!

          1. Haha – I noticed that with the table names and I’m with you. Always name my tables singular, was nice to see it!

            As for the other stuff, just working through your code now, there’s a few little tweaks I’m making here and there to get it working but it’s really good and will be a big help to me in getting done what I want – thanks very much!

            Out of interest, once you install your module and you have the main ‘Students’ many category on the left, did you remove the ‘Users’ category to avoid confusion?

            I think I’m going to go down a similar route to you for all members – gathering extended profile information and create a new ‘Members’ module but not sure whether to place the admin menu item under the users section and remove the ‘Manage Users’ link or have an extra one in there – one for the extended info and one for the standard manage users section. Guess it would be nice to manage all users from one link as you’ve done with the students module (hence why I wondered if you did away with the standard Users management section)

          2. I did not remove my users section, mainly because I still needed to create admin users and other types. I also added some more features to the students module (not released) and so having the users as a fall back to verify things has been most helpful.

            Eventually I’d like to write an “extended property” manager that could potentially take away the users menu, but all in all I think it’s best to keep it there for core admin type stuff, unless your end users would be too confused by the two.

  5. Hello,

    I am working on a project similar to yours . I installed your module and it went fine…When i tried accessing it i got this error…

    A Database Error Occurred

    Error Number: 1054

    Unknown column ‘profiles.last_name’ in ‘field list’

    SELECT default_student.age, default_student.grade_level, default_student.esl, default_student.sport_id, default_student.sport_level_id, default_profiles.*, default_users.*, g.description as group_name, IF(profiles.last_name = “”, default_profiles.first_name, CONCAT(profiles.first_name, ” “, default_profiles.last_name)) as full_name FROM (default_student) LEFT JOIN default_users ON = default_student.user_id LEFT JOIN default_groups g ON = default_users.group_id LEFT JOIN default_profiles ON default_profiles.user_id = WHERE `g`.`name` = ‘student’ GROUP BY ORDER BY desc LIMIT 25

    Filename: C:\wamp\www\pyrocms_working\system\codeigniter\database\DB_driver.php

    Line Number: 330

    any help???

    1. Francia

      This module was really helpful and I implemented similar. I think you’ll find the problems to do with the multi-site install as mentioned below. The first culprit is likely to be line 21 in students_m.php:

      select('IF('.$this->profile_table.'.last_name = "", '.$this->profile_table.'.first_name, CONCAT('.$this->profile_table.'.first_name, " ", '.$this->profile_table.'.last_name)) as full_name', FALSE)

      Work through the MySQL errors and they will more than likely be to do with the profile table not having default_prefix. The multi-site upgrade to Pyro did cause a few headaches with various add-on modules.

      1. Hello richlovelock,thanks for your response.. I still keep getting errors… this is the code… maybe there is something am doing wrong (am still new to pyrocms and codeigniter as a whole
        class Students_m extends MY_Model
        protected $_table = ‘student’;

        function get_all()
        $this->db->select(‘student.age, student.grade_level, student.esl, student.sport_id, student.sport_level_id, profiles.*, users.*, g.description as group_name, IF(profiles.last_name = “”, profiles.first_name, CONCAT(profiles.first_name, ” “, profiles.last_name)) as full_name’)
        ->join(‘users’, ‘ = student.user_id’, ‘left’)
        ->join(‘groups g’, ‘ = users.group_id’, ‘left’)
        ->join(‘profiles’, ‘profiles.user_id =’, ‘left’);

        $this->db->where(‘’, ‘student’);


        return parent::get_all();

        // Create a new student
        function add($input = array())

        // Do initial insert and get object
        $user = parent::insert(array(
        ’email’ => $input->email,
        ‘password’ => $input->password,
        ‘salt’ => $input->salt,
        ‘first_name’ => ucwords(strtolower($input->first_name)),
        ‘last_name’ => ucwords(strtolower($input->last_name)),
        ‘role’ => empty($input->role) ? ‘user’ : $input->role,
        ‘is_active’ => 0,
        ‘lang’ => $this->config->item(‘default_language’),
        ‘activation_code’ => $input->activation_code,
        ‘created_on’ => now(),
        ‘last_login’ => now(),
        ‘ip’ => $this->input->ip_address()

        $student = $user;

        // Add student information and insert it as well.
        $student->age = $input->age;
        $student->grade_level = $input->grade_level;
        $student->gender = $input->gender;
        $student->esl = $input->esl;
        $student->sport_id = $input->sport_id;
        $student->sport_level_id = $input->sport_level_id;

        ‘user_id’ => $user->id,
        ‘age’ => $student->age,
        ‘grade_level’ => $student->grade_level,
        ‘gender’ => $student->gender,
        ‘sport_id’ => $student->sport_id,
        ‘sport_level_id’ => $student->sport_level_id,
        ‘created’ => ‘NOW()’,
        ‘modified’ => ‘NOW()’

        $student->id = $this->db->insert_id();

        return $student;

        public function count_by($params = array())
        $params[‘active’] = $params[‘active’] === 2 ? 0 : $params[‘active’] ;
        $this->db->where(‘’, $params[‘active’]);

        $this->db->where(‘group_id’, $params[‘group_id’]);

        $this->db->like(‘users.username’, trim($params[‘name’]))
        ->or_like(‘’, trim($params[‘name’]))
        ->or_like(‘profiles.first_name’, trim($params[‘name’]))
        ->or_like(‘profiles.last_name’, trim($params[‘name’]));

        return $this->db->count_all_results();

        public function get_many_by($params = array())
        $params[‘active’] = $params[‘active’] === 2 ? 0 : $params[‘active’] ;
        $this->db->where(‘’, $params[‘active’]);

        $this->db->where(‘group_id’, $params[‘group_id’]);

        $this->db->or_like(‘users.username’, trim($params[‘name’]))
        ->or_like(‘’, trim($params[‘name’]))
        ->or_like(‘profiles.first_name’, trim($params[‘name’]))
        ->or_like(‘profiles.last_name’, trim($params[‘name’]));

        return $this->get_all();

        1. Greetings Francia,

          Have you tried replacing the instances of “profiles.[ABC]” with “default_profiles.[ABC]” yet? The other thing that might help is to check the system/cms/logs folder, which might have a few more details to debug.

          Looks like I need to go back and update this code over the holidays 8^D

  6. Greetings Francia,

    I had published this library shortly before PyroCMS was upgraded to version 1.4, which allowed for multi-site support. This update renamed the tables to be default_[table name] for a single instance install. I had updated a few things, but now all of them.

    While it’s hard to find exactly where the error is occuring based on the error, do a search through the entire solution for profiles.last_name and make sure update the table name to be “default_profile” and see if that resolves your issue. Some of the queries are “Active Record” based, which auto prepend this, some are not.

    Another thing to check, if you were upgrading your system, is to make sure that your database has the “default_profiles” table in the first place. I ran into one instance on an old upgrade where the tables weren’t renamed, so I had to redo my process.

    Hope this helps!

    1. Hi,
      I would like to ask a question slightly different. Pyrocms uses a defaul_ database prefix … doesn’t this pose a security threat like that of jos_ in joomla

      1. It could, and I know that’s how a lot of WordPress hacks have worked in the past, exploiting the lack of table prefix in the table names.

        However, that would be a better question answered by the PyroCMS or the CodeIgniter folks, from which PyroCMS was based. I do know that by default they use Active Record, which to my knowledge helps prevent a lot of SQL Injection routes. They also include data input sanitizing methods for both SQL and XSS as well. But your best bet would be to check the forums over there. Hope this helps!

  7. Dillie-O

    I like the concept and would like to leverage the code. I am using Pyro 2, have you had a chance to update your code as yet? The holidays are over ;-)

    1. Indeed David, the holidays are over 8^D I have not had a chance to download version 2 yet and plug it in. I should do that here soon. I’m glad you like the concept though. I’ll make sure to post an update when I get it done.

  8. Hi Dillie-O,

    Its great. this is the 2nd module i found about pyrocms. before this i was looking at “sample module”. now your “student module” make many things clear, here when i try to install plugin, i am getting following error.

    Fatal error: Call to undefined method Admin_Controller::Admin_Controller() in C:\xampp\htdocs\projects\pyro2\addons\default\modules\Students\controllers\admin.php on line 94

    any guess?

    on line 94 i have this code:

    public function __construct()
    // Call the parent’s constructor method

    // Load the required classes

    and so on….

    1. One quick to note, while I’m working on a full update, the constructor for the admin controller can now be called by simply calling parent::__construct() instead of Admin_Controller()

      Give that a whirl and see if it helps.

  9. Greetings bytehow,

    I’m wagering this has something to do with the changes in version 2.0, since when I wrote this extension it was version 1.1. I’m actually firing a small project that will use PyroCMS version 2.0, so I’ll take a peek at the module again and make the proper updates. I’ll have some updates soon.

  10. Thank you for this, Dillie-O.

    Starting from your idea, I’ve been actually able to extend the Users module. What I did was the following:

    1) Create a new module in ‘addons/default/modules/extusers’

    2) Copy the folders ‘config’, ‘img’, ‘language’ and ‘views’ from the original Users module to the new one. Then change them as you like

    3) Make empty folders ‘controllers’, ‘libraries’ and ‘models’ and create there your own classes extending the original ones

    4) Create in the new module anything else you need (in my case I needed some css and js files)

    5) Very important: Modify the rounting file in system/cms/config/routes.php so that everything trying to go to ‘Users’ will actually goes to ‘Extusers’.
    In my case it looks something like this:

    // User routes
    $route['register'] = extusers/register';
    $route['user/([\w]+)']	= 'extusers/view/$1';
    $route['my-profile']	= 'extusers/index';
    $route['edit-profile']	= 'extusers/edit';
    $route['users/(:any)']	= 'extusers/$1';
    $route['users']	= 'extusers';

    I hope this can help you guys.

    1. Excellent!!!! Thanks for sharing this. Was it that difficult once you copied the files over to get your added fields? Was there any other problems you ran into along the way?

      1. Hi! Yes, I actually had a problem.
        It is that our parent Class (Users) is still looking for some files, for instance, the Model file user_m.php which is not in our new Extusers module.
        So, what I did is just create a file called user_m.php in our new models folder and bring the original user_m.php like this:

        [/sourcecode ]
        Did you tried it? Did you find anything else?

  11. Wow that was strange. I just wrote an very long comment but after I clicked submit my comment didn’t
    show up. Grrrr… well I’m not writing all that over again.
    Anyhow, just wanted to say fantastic blog!

What are your 10 bits on the matter? I want to know!

Fill in your details below or click an icon to log in: Logo

You are commenting using your account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s