enygma/modler

A basic model library

2.1.4 2016-05-18 21:04 UTC

This package is auto-updated.

Last update: 2022-09-15 06:21:46 UTC


README

Build Status

Modler is a set of scripts that provide some of the most basic model and collection handling pieces of functionality.

Installation

You can install the library via Composer using the require command:

composer require enygma/modler

Classes

Model

The Model class helps to model out an object with properties and relationships. A model is defined with a set of properties that can either be literal values or relationships to other models. Here's an example of a simple model:

<?php

class TestModel extends \Modler\Model
{
    protected $properties = array(
        'test' => array(
            'description' => 'Test Property 1'
            'type' => 'varchar'
        ),
        'relateMe' => array(
            'description' => 'Relation Property 1',
            'type' => 'relation',
            'relation' => array(
                'model' => '\\MyApp\\OtherModel',
                'method' => 'findByTestValue',
                'local' => 'test'
            )
        )
    );
}

?>

In this example there's two properties set: test and relateMe. The test is a simple property, allowing for easy getting and setting of the data:

<?php

$model = new TestModel();
$model->test = 'foo';

?>

Relational Properties

The relational property relateMe is a little more complicated. It's defined as a type of "relation" and the relation configuration contains three pieces of information:

  • The model to call (full namespacing is better here)
  • The method to call on the other model
  • The local property to send as a parameter

All properties are lazy loaded, so you're only going to execute the call on relateMe if you use it in your code:

<?php

$model = new TestModel();
$otherModel = $model->relateMe;

?>

When the relateMe property is used, the findByTestValue method is called on an instance of the OtherMethod class with the value currently in the test property. If none is defined, null is passed. The resulting model is then returned. So, you can do fun chaining things like:

<?php

$model = new TestModel();
echo $model->relateMe->otherModelProperty;

?>

If the findByTestValue method uses the load method inside of it to load the data, you should get a value back if it's found:

<?php

class OtherModel extends \Modler\Model
{
    protected $properties = array(
        'otherModelProperty' => array(
            'type' => 'varchar',
            'description' => 'Just another property'
        )
    );

    public function findByTestValue($value)
    {
        /** some SQL that extracts the data */
        $this->load($result);
    }
}

?>

Remember, when you use the properties, you're going to get back the updated model instance, not the return value for the method that was called. You'd need to access properties on the returned model to get data from it.

Relations "Return Value"

You can also have a relation return a value instead of an instance of a class or model. To do so, just set the return value to "value" and use the normal return from your method:

<?php
class TestModel extends \Modler\Model
{
    'property1' => array(
        'description' => 'Property #1',
        'type' => 'varchar'
    ),
    'relateMe' => array(
        'description' => 'Return by value relation',
        'type' => 'relation',
        'relation' => array(
            'model' => '\\MyApp\\OtherModel',
            'method' => 'findByTestReturnValue',
            'local' => 'test',
            'return' => 'value'
        )
    )
}

class OtherModel extends \Modler\Model
{
    public function findByTestReturnValue($property1)
    {
        return 'this is a value';
    }
}

// Then, in the "relateMe" property we'd get...
if ($testModel->relateMe === 'this is a value') {
    echo 'Valid match!';
}

?>

In this case we're calling \MyApp\OtherModel::findByTestReturnValue and asking for a return value rather than getting back the model with a value set.

Data Verification

Modeler can also do some simple data verification around required values. You can set the required state in the property configuration:

<?php
class OtherModel extends \Modler\Model
{
    protected $properties = array(
        'prop1' => array(
            'description' => 'Property #1 (Required)',
            'type' => 'varchar',
            'required' => true
        )
    );
}

$other = new OtherModel();
try {
    $other->verify();
} catch (\Exception $e) {
    echo 'ERROR: '.$e->getMessage();
}
?>

If a value is not set (checked with isset) an exception will be thrown with information on which property was missing. If you ever find a place where you're getting an exception but you want a value ignored from the verification, you can use the optional parameter to the verify method:

<?php

$ignore = array('prop1');
try {
    $other->verify($ignore);
    echo 'Success!';
} catch (\Exception $e) {
    echo 'Never gets here.';
}

?>

Using the MySQL Model

Modler also comes with a MySQL version of the model class that contains some common database functionality. It includes some of the basics you'd need for working with models and loading them from a database. It follows a sort of Active Record methodology with a few differences. Here's how to make a simple MySQL model:

<?php

class UserModel extends \Modler\Model\Mysql
{
    protected $tableName = 'users';

    protected $properties = [
        'username' => ['column' => 'username']
    ];
}

?>

You can see a few key differences here: first is the tableName property defining what MySQL table the model needs to work with, second is the column attribute in the properties. This links the field to a column in the database and lets Modler know where to load the data.

You can then use most of the expected functionality to load and work with objects:

<?php

$db = new PDO('mysql:hostname=127.0.0.1;dbname="testdb', 'username', 'password');
$user = new UserModel($db);

// Finding a user by the "id" column, loads the data into the user
$user->findById(1);

// Or using custom criteria
$user->find(['username' => 'foobar']);

// Creating a new user
$user = new UserModel($db);
$user->username = 'newuser1';
$user->save();

// Updating the username property and saving
$user->username = 'foobarbaz';
$user->save();

// Deleting a user
$user = new UserModel($db);
$user->findById(1);
$user->delete();
?>

You'll notice that, before we can do anything with a user object, we need to call one of the two find methods on it, either findById or just find. This will locate the user details and pull them into the current object. Otherwise Modler has no idea what to operate on.

Collection

The Collection class is designed to be a set of models. It's a generic container that can be iterated over. There's no special logic in it that requires the contents to be a model (or even and object for that matter).

They're pretty straightforward to use:

<?php

$hats = new HatsCollection();
$hats->add(array('test' => 'foo'));

foreach ($hats as $value) {
    print_r($value); // will output the "test" => "foo" data
}

echo 'Items in collection: '.count($hats)."\n";

?>

There is one interesting thing that you can do with the Modler models if you use them in this collection. The example above mentions the toArray method on the Model class. The Collection has one too, but it includes an expand parameter. If this parameter is set to true (false by default) it will call the toArray method on each item in the collection and return those results. For example:

<?php
$hat1 = new HatModel();
$hat1->type = 'fedora';

$hat2 = new HatModel();
$hat2->type = 'sombrero';

$hats = new HatsCollection();
$hats->add($hat1);
$hats->add($hat2);

$result = $hats->toArray(true);

/**
 * Result will be array(
 *     array('type' => 'fedora'),
 *     array('type' => 'sombrero')
 * )
 */
?>

The Hats models will have the toArray called on them too, translating them into their array versions and appended for output.

Much like in the models, custom collections methods will probbly not want to return a value. Insted they should populate out the data where they live. See the next section for an example that'll probably make more sense.

Using the MySQL Collection

Much like the MySQL model that's included with Modler, there's also a simple MySQL collection. Mostly it's a convenience method for getting data and making it eaier to load into the database. As seen in the example below (findColorsByType), you can easily push the results of a query into a set of objects and add them to the collection. Here's an example:

<?php

class UserCollection extends \Modler\Collection\Mysql
{
    public function findUsersByType($type)
    {
        $sql = 'select username from users where type = :type';
        $results = $this->fetch($sql, array('type' => $type));

        foreach ($results as $result) {
            $user = new Usermodel($this->getDb(), $result);
            $this->add($user);
        }
    }
}

?>

You'll then have a set of UserModel objects that match the type you've queried for. Note that the query handling uses bound parameters/prepared statements for execution.

Combining them

Lets combine them using the relations the models offer and populate a collection:

<?php

class HatsCollection extends \Modler\Collection
{
    public function findColorsByType($type)
    {
        foreach ($results as $result) {
            $color = new \Modler\Color($result);
            $this->add($color);
        }
    }
}

class HatModel extends \Modler\Model
{
    protected $properties = array(
        'type' => array(
            'description' => 'Hat Type',
            'type' => 'varchar'
        ),
        'colors' => array(
            'description' => 'Hat Colors',
            'type' => 'relation',
            'relation' => array(
                'model' => '\\Modler\\HatsCollection',
                'method' => 'findColorsByType',
                'local' => 'type'
            )
        )
    );
}

$hat = new HatModel();
$hat->type = 'toque';

// This returns a Hats Collection with the data populated
$colors = $hat->colors;

?>

Now when we access the $hat->colors property, we'll get back an instance in $colors of the HatsCollection with the data loaded. You can then use it just like a normal collection and use toArray on it to get the contents.

Filtering

Collections also allow you to do some basic filtering based on custom logic. You can use the filter method to to a pass/fail check on the data and add/remove things from the collection. Here's an example:

<?php

$this->collection->add(array('foo' => 'bar'));
$this->collection->add(array('baz' => 'test'));

$filtered = $this->collection->filter(function($value) {
    return (isset($value['foo']));
});

print_r($filtered);

?>

In this case, we're checking to see if the foo array key is set. It's only set in the one case (with the value of "bar") so the resulting collection in filtered will only contain this one element. The callable function you pass in should take one parameter, the value, and should return a boolean for the pass/fail status of the check. Values cannot be modified through this method.

Slicing

You can also get portions of the collection data without having to export it all using toArray and working with it there. The slice method makes it easy to get just the portion of the data you want. The first parameter is the start index and the second (optional) is how many items to return. It works using the array_slice function.

<?php

$this->collection->add('foo');
$this->collection->add('bar');
$this->collection->add('baz');
$this->collection->add('test');

// This will return: array('bar', 'baz', 'test')
$sliced = $this->collection->slice(1);

// This will return: array('baz')
$sliced = $this->collection->slice(2, 1);

?>
```

### Guarding and Array Filtering

`Modler` also includes two other concepts around the contents of the model data: guarding and filtering (not the same as the `filter` method above).

**Guarding** lets you protect properties from being set when the `load` method is called or when set as a property on the object. This helps to prevent mass assignment security issues. For example, we make a model with two properties and guard the `id` value so it can never be overriden:

```php
<?php

class HatModel extends \Modler\Model
{
    protected $properties = array(
        'id' => array(
            'description' => 'Hat ID',
            'type' => 'integer',
            'guard' => true
        ),
        'type' => array(
            'description' => 'Hat Type',
            'type' => 'varchar'
        )
    );
}

// Now if we try to set it...
$hat = new HatModel();
$hat->id = 1234;

var_export($hat->id); // result is NULL

// Calling load has the same effect
$hat->load(array('id' => 1234));

?>
```

There is a second optional paramater for the `load` method that turns off this guarding and allows the setting of all values (useful when loading objects from a datasource):

```php
<?php

$data = array('id' => 1234);
$hat->load($data, false);

var_export($hat->id); // result is 1234
?>
```

The other feature, array filtering, works with the `toArray` method and lets you filter out values when you call it. This can be useful if you have an object with sensitive data like password hashes that don't need to be stored. Here's an example:

```php
<?php

class UserModel extends \Modler\Model
{
    protected $properties = array(
        'username' => array(
            'description' => 'Username',
            'type' => 'varchar'
        ),
        'password' => array(
            'description' => 'Password',
            'type' => 'varchar'
        )
    );
}

// To get the result without the password, we call it with the filter paramater
$user = new UserModel(
    array('username' => 'foobar', 'password' => password_hash('foobar'))
);

$result = $user->toArray(array('password'));
print_r($result); // results in an array with just array('username' => 'foobar')
?>
```

### Array "taking" and ordering

There's also a similar interface to the `slice` method on Modler collections called `take`. With slice you can get more specific with the section of the results but if you need something simpler, you can just "take" a few off the top:

```php
<?php
// Add a bunch of items to our collection
$data = array('foo', 'bar', 'baz', 'quux', 'foobar', 'barbaz');
$collection = new \Modler\Collection();
foreach ($data as $value) {
    $collection->add($value);
}

// You can limit the current collection and return
$collection = $collection->take(3);

 // this returns 3
echo 'count: '.count($collection);

// This returns Array('foo', 'bar', 'baz')
print_r($collection->toArray());

?>
```

**NOTE:** The collection returned is a new instance. The data in the previous collection is untouched.

You can also *sort* the data in the current collection (this does modify the collection dataset) either as a set of strings or by a property on an object. First, as strings:

```php
<?php
// Add a bunch of items to our collection
$data = array('foo', 'bar', 'baz', 'quux', 'foobar', 'barbaz');
$collection = new \Modler\Collection();
foreach ($data as $value) {
    $collection->add($value);
}

$collection->order();

// returns Array('bar', 'barbaz', 'baz', 'foo', 'foobar', 'quux');
print_r($collection->toArray());
?>
```

The default sort order is descending, but you can tell it to sort ascending:

```php
<?php
$collection->order(\Modler\Collection::SORT_ASC);
?>
```

And, if you want to sort objects, you provide the name of the property as the second parameter:

```php
<?php
// Add a bunch of items to our collection
$data = array('foo', 'bar', 'baz', 'quux', 'foobar', 'barbaz');
$collection = new \Modler\Collection();
foreach ($data as $value) {
    $collection->add($value);
}

$collection->order(\Modler\Collection::SORT_DESC, 'test');

/**
 * Returns set of objects ex:
 * [0] => stdClass Object
 *  ( [test] => bar )
 *
 * [1] => stdClass Object
 *  ( [test] => barbaz )
 *
 * [2] => stdClass Object
 *  ( [test] => baz )
 */
print_r($collection->toArray());
?>
```