PHP
Article

Contributing to Open Source: Gatekeeper Case Study

By Bruno Skvorc

GateKeeper is a pretty nifty user registration, authentication and authorization library which uses its own database to store and query the user records. This means that it’s completely decoupled from your main app (so you can, essentially, use Postgre or Mongo for your business logic while using a completely different engine like MySQL for basic user records) and easy to tweak and extend.

Open source logo

An example of the library’s use is clearly demonstrated in our post about [the skeleton no-framework project][nowf] which is a sample app composed entirely of Composer packages and acting like a framework-powered app, but completely free of any framework coupling.

This post isn’t about Gatekeeper per-se, though. It’s about contributing to open source, and going about the right way to doing it.

In this tutorial, we’ll extend GateKeeper with a count feature. Currently, in order to find out the total number of users in the database one would have to fetch them all, then count them – either that or write a query to do so manually. But it might be better if this were built into the adapter interface so that it’s not only a native feature, but also a requirement for future database engines to be added.

Step 1: Ask the owner

The first step of contributing to open source is doing due diligence. This can be as simple as asking the repo owner about the status of this feature, in order to make sure it isn’t planned and is, in fact, desirable. An issue in the repo is often enough, as evident in this case.

Step 2: Fork, clone, test

Note: if you’d like to follow along, please clone an older version of Gatekeeper which doesn’t have this feature yet. This one should do.

First, let’s fork the repo so we can start working on it.

Fork button on Github

Next, we need to set up a development environment in which to work on the package. Naturally, we use our trusty Homestead Improved for this. Once the VM has been set up and SSHed into, we can clone our fork, install dependencies and run tests:

git clone https://github.com/swader/gatekeeper
cd gatekeeper
composer install
vendor/bin/phpunit

All the tests should be green:

At this point, it’s preferred to make a separate branch for all the changes we’ll be making.

git checkout -b "feature-count"

Step 3: Plan of Action

Gatekeeper currently only supports MySQL – this makes our job a bit easier, but still not trivial. Despite only supporting a single type of datasource (for now), abstract and interface classes still need to be updated, seeing as they’re written with future compatibility with different data sources in mind.

We will, thus, need to modify:

  • Gatekeeper/DataSource – the abstract DataSource class
  • DataSource/MySQL – the MySQL datasource which contains the actual methods we use
  • DataSource/Stub – to upgrade the stub with which to write other datasources, so other contributors know they need a count method, too

We also need to create a new Count handler, because Gatekeeper uses magic static calls to create, find, update and delete entities, forwarding them to the appropriate handler depending on the name of the invoked method. For an example, see the __callStatic magic method in Gatekeeper/Gatekeeper.php, and how it defers method calls to handlers like Handler/Create.php or Handler/FindBy.php, etc.

Step 4: Just Do It ™

Delegating the static call

To prepare the foundation for our custom Count handler, we delegate the static call to it and pass forward the argument and the data source. This is all done by simply adding another elseif block to Gatekeeper::__callStatic:

        } elseif ($action == 'count') {
            $action = new \Psecio\Gatekeeper\Handler\Count($name, $args, self::$datasource);
        }

Since we added a new action, we need to modify the static property $actions as well:

    /**
     * Allowed actions
     * @var array
     */
    private static $actions = array(
        'find', 'delete', 'create', 'save', 'clone', 'count'
    );

Count handler

We then create the handler in the file Psecio/Gatekeeper/Handler/Count.php:

<?php

namespace Psecio\Gatekeeper\Handler;

class Count extends \Psecio\Gatekeeper\Handler
{
    /**
     * Execute the object/record count handling
     *
     * @throws \Psecio\Gatekeeper\Exception\ModelNotFoundException If model type is not found
     * @return int Count of entities
     */
    public function execute()
    {
        $args = $this->getArguments();
        $name = $this->getName();

        $model = '\\Psecio\\Gatekeeper\\' . str_replace('count', '',
                $name) . 'Model';
        if (class_exists($model) === true) {
            $instance = new $model($this->getDb());

            $count = (!$args) ? $this->getDb()->count($instance) : $this->getDb()->count($instance,
                $args[0]);
            return (int)$count['count'];
        } else {
            throw new \Psecio\Gatekeeper\Exception\ModelNotFoundException(
                'Model type ' . $model . ' could not be found'
            );
        }
    }
}

It’s almost identical to the Create handler, except for the unreachable return statement at the bottom which I’ve removed, small changes in the body, and a minor alteration to the class’ description.

DataSource and Stub

Next, let’s get the easy ones out of the way.

In Psecio/Gatekeeper/DataSource/Stub.php, we add a new blank method:

    /**
     * Return the number of entities in DB per condition or in general
     *
     * @param \Modler\Model $model Model instance
     * @param array $where
     * @return bool Success/fail of action
     * @internal param array $where "Where" data to locate record
     */
    public function count(\Modler\Model $model, array $where = array()){}

We then add a similar signature to the abstract:

    /**
     * Return the number of entities in DB per condition or in general
     *
     * @param \Modler\Model $model Model instance
     * @param array $where
     * @return bool Success/fail of action
     * @internal param array $where "Where" data to locate record
     */
    public abstract function count(\Modler\Model $model, array $where = array());

With all this out of the way, it’s time to write the actual logic that takes care of counting.

MySQL

It’s time to change DataSource/MySQL.php now. We’ll add the count method right under the find method:

    /**
     * Find count of entities by where conditions.
     * All where conditions applied with AND
     *
     * @param \Modler\Model $model Model instance
     * @param array $where Data to use in "where" statement
     * @return array Fetched data
     */
    public function count(\Modler\Model $model, array $where = array())
    {
        $properties = $model->getProperties();
        list($columns, $bind) = $this->setup($where);

        $update = array();
        foreach ($bind as $column => $name) {
            // See if we keep to transfer it over to a column name
            if (array_key_exists($column, $properties)) {
                $column = $properties[$column]['column'];
            }
            $update[] = $column.' = '.$name;
        }

        $sql = 'select count(*) as `count` from '.$model->getTableName();
        if (!empty($update)) {
            $sql .= ' where '.implode(' and ', $update);
        }

        $result = $this->fetch($sql, $where, true);
        return $result;
    }

Harvesting the logic from the similar find method above, our count method does the following:

  1. Grab properties as defined in model in question (e.g. see UserModel::$properties)
  2. Separate the values as passed in via $where into columns and their values
  3. Build the WHERE part of the query by looking into the properties, seeing if any of them have different names to those requested (e.g. requested FirstName has a database counterpart of first_name)
  4. Build whole query
  5. Execute with forced single mode on true (see fetch method) because we only expect to get a single value back – an integer indicating the count.
  6. Return the count.

Step 5: Testing

Ordinarily, there would be a unit testing stage. This is out of the scope of this tutorial, and I encourage you to look at this tutorial instead. If there’s sufficient interest in seeing unit tests developed for this package, we will, of course, accommodate. Let us know in the comments.

Implementing the Experimental Version

Let’s do a manual test. First, we’ll commit and push our work online.

git add -A
git commit -m "Adding count feature"
git push origin feature-count

The changes will now be in our fork, online. Then, let’s go ahead and create a brand new project in another folder with the following composer.json file:

{
    "require": {
        "psecio/gatekeeper": "dev-master"
    },
    "repositories": [
        {
            "type": "vcs",
            "url": "https://github.com/swader/gatekeeper"
        }
    ]
}

Using Composer’s Repositories feature, we can make sure that Composer fetches our copy of Gatekeeper instead of the original, while still thinking it has the original – this allows us to test our changes as if in a real project using Gatekeeper – arguably a more realistic testing scenario than unit tests would be at this point. Save and exit this file, and then run:

composer require symfony/var-dumper --dev

This will both install the above defined custom package, and Symfony’s VarDumper for easier debugging. You might get asked for a Github token during installation – if that’s the case, just follow the instructions.

Lo and behold, if we look inside the Gatekeeper main class now, we’ll see that our count updates are all there. Next, let’s follow the typical Gatekeeper installation procedure now by executing vendor/bin/setup.sh and following instructions. If you’re using Homestead Improved, just enter the default values (localhost, homestead, homestead, secret).

Testing

Now let’s create an index.php file which we’ll use for testing:

<?php

require_once 'vendor/autoload.php';

use Psecio\Gatekeeper\Gatekeeper;
Gatekeeper::init('./');

$groups = [
    'admin' => 'Administrators',
    'users' => 'Regular users'
];

foreach ($groups as $name => $description) {
    if (!Gatekeeper::findGroupByName($name)) {
        Gatekeeper::createGroup([
            'name' => $name,
            'description' => $description
        ]);
    }
}

We activate the autoloader, initialize Gatekeeper (it uses the .env file from the root of our folder for credentials), and set up two default groups.

Sequel Pro Screenshot of the two groups

Then, let’s go ahead and test counting on the groups:

dump(Gatekeeper::countGroup());
dump(Gatekeeper::countGroup(['id' => 1]));

Sure enough, it works.

Group count accurate

Let’s test users now.

Gatekeeper::countUser();

This accurately produces a count of 0. A common use case for out-of-the-box apps is seeing if there are no users in the database when a user is being created, and then giving that new user Admin privileges. The first user of a system is often considered its owner, so it’s a convenience for setting up super-user accounts. Let’s do that.

Gatekeeper::createUser([
    'username' => 'bruno-admin',
    'first_name' => 'Bruno',
    'last_name' => 'Skvorc',
    'email' => 'bruno.skvorc@sitepoint.com',
    'password' => '12345',
    'groups' => (Gatekeeper::countUser()) ? ['users'] : ['admin']
]);

Gatekeeper::createUser([
    'username' => 'reg-user',
    'first_name' => 'Reggie',
    'last_name' => 'User',
    'email' => 'reg@example.com',
    'password' => '12345',
    'groups' => (Gatekeeper::countUser()) ? ['users'] : ['admin']
]);


dump(Gatekeeper::findUserByUsername('bruno-admin')->groups[0]->name);
dump(Gatekeeper::findUserByUsername('reg-user')->groups[0]->name);

Sure enough, the output is accurate – the first group printed on screen is admin, and the second is users.

With the count mechanics correctly implemented, it’s time to submit a pull request.

Submitting a PR

First, we go to our own fork of the project. Then, we click the “New Pull Request” button. On the next screen everything should be green – the UI should say “Able to merge”:

Able to merge

Once we click the “Create Pull Request” button, we should add a title and a description as detailed as possible, preferably referencing the issue from Step 1 above.

Creating the PR

That’s it – pressing the “Create Pull Request” button wraps things up – all we can do now is wait for feedback from the project owner.

Conclusion

This was a case study of contributing to a relatively popular PHP package. I hope it was useful as a learning material and a basic guide into giving back and the process of adding features into something you use.

It’s important to note that while this is a common process, as I mentioned before, it’s rather uncommon in that it had no unit tests. However, the original library doesn’t test the handlers either, and making a mocking framework for the handlers and the data they can interact with would be too out of scope for this particular post. Again, if you’re curious about that process and would like us to cover it in depth, please do let us know in the comments below that like button.

It should also be noted that while this was a very easy feature upgrade, sometimes things don’t go according to plan and the changes won’t work immediately in the new project where they’re being tested. In that case, changes can, for example, be made in the vendor folder of the test project until everything works, copied over to the development project, pushed online and then re-installed into the test project to make sure everything still works.

Have your own contribution stories to share? Let us know – we’d love to explore them!

Meet the author
Bruno is a coder from Croatia with Master’s Degrees in Computer Science and English Language and Literature. He’s the editor of SitePoint’s PHP channel and a developer evangelist for Diffbot.com. He avoids legacy code like the plague and when picking projects makes sure they’re as cutting edge as possible. He’s a treadmill desk enthusiast and active (board)gamer who sometimes blogs.
  • http://heera.it/ Sheikh Heera

    This is really a nice and very helpful article. It’s a different kind of article where you’ve focused on the technical parts that require for contributing on an open source project using Git & Github. I often see people use Git/Github for their projects but don’t understand the workflow properly and this one would be really helpful to many who wants to contribute and able to code but don’t know how to contribute.

    Anyways, probably you could have mentioned about CI (Continuous Integration) a little bit with links, so it could have at least, lead some people (who doesn’t know) to a new direction because most of the times we all need a clue. All the best :-)

    • http://www.bitfalls.com/ Bruno Skvorc

      Glad you liked it, and thanks for the feedback, I’ll keep it in mind! We do have some posts about CI here: http://www.sitepoint.com/tag/ci/

  • Anand Mallick

    It is a nice article to give a head start in the world of open source to beginners like me. An in depth article would be great…

    • http://www.bitfalls.com/ Bruno Skvorc

      I’d love to accommodate – but in depth into what exactly? What’s most foreign / confusing to you?

      • Anand Mallick

        What should one do in case of merge conflicts? Do I have to rebase my code? Also, how do I keep my fork in sync with the original repository?

        • http://www.bitfalls.com/ Bruno Skvorc

          In case of merge conflicts, you switch to the master branch, git pull to pull the latest original, switch to your branch again, and then do `git merge master` to merge master into the branch you’re currently on. Then, you resolve conflicts by going through them one by one (and decent code editor will help you out there by guiding you).

          As for keeping a fork in sync, here’s the GUI way: http://www.sitepoint.com/quick-tip-sync-your-fork-with-the-original-without-the-cli and here’s the CLI way: http://www.sitepoint.com/quick-tip-synch-a-github-fork-via-the-command-line/

          You see, we’ve covered all the bases :)
          But please, do ask if anything else is unclear – we’d love to make things more intuitive all around.

          • Anand Mallick

            Thanks for the help. Keep up the good work :-)

Recommended

Learn Coding Online
Learn Web Development

Start learning web development and design for free with SitePoint Premium!

Get the lastest in PHP, once a week, for free.