Skip to content

Commit

Permalink
Initial Commit
Browse files Browse the repository at this point in the history
  • Loading branch information
liam-wiltshire committed Apr 17, 2019
1 parent 89b3993 commit a4af637
Show file tree
Hide file tree
Showing 13 changed files with 633 additions and 1 deletion.
37 changes: 37 additions & 0 deletions .circleci/config.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
# PHP CircleCI 2.0 configuration file
#
# Check https://circleci.com/docs/2.0/language-php/ for more details
#
version: 2
jobs:
build:
docker:
# Specify the version you desire here
- image: circleci/php:7.1-node-browsers

steps:
- checkout

- run: sudo apt update # PHP CircleCI 2.0 Configuration File# PHP CircleCI 2.0 Configuration File sudo apt install zlib1g-dev libsqlite3-dev
- run: sudo docker-php-ext-install zip

# Download and cache dependencies
- restore_cache:
keys:
# "composer.lock" can be used if it is committed to the repo
- v1-dependencies-{{ checksum "composer.json" }}
# fallback to using the latest cache if no exact match is found
- v1-dependencies-

- run: composer install -n --prefer-dist

- save_cache:
key: v1-dependencies-{{ checksum "composer.json" }}
paths:
- ./vendor

# run tests with phpunit
- run: ./vendor/bin/phpunit
- run: ./vendor/bin/phpcs --standard=PSR2 src/
- run: php tests/CoverageCheck.php tests/clover.xml 95

6 changes: 6 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
.idea
.DS_store
/vendor/
composer.lock
/tests/clover.xml
/tests/reports
8 changes: 8 additions & 0 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
# Contributing

This will be more fleshed out, in the coming weeks, but essentially:

1. Code should adhere to PSR-2 standards please
2. Please include unit tests for any significant code changes
3. Please ensure code changes do not break existing tests
4. Changes that don't fit with the project goals may be rejected - if in doubt, ask first :-).
21 changes: 21 additions & 0 deletions LICENSE.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
The MIT License (MIT)

Copyright (c) 2019 Liam Wiltshire

Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
68 changes: 67 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
@@ -1 +1,67 @@
# laravel-model-meta
# liam-wiltshire/laravel-model-meta

liam-wiltshire/laravel-model-meta is an extension to the default Laravel Eloquent model to add metadata to a model.

# What MetaData?
While Eloquant is very good at handling relational data, however not all data we deal with works like this. Without going as far as using a NoSQL solution such as Mongo, using mySQL to hold relational data, but with a JSON meta field is a potential solution.

liam-wiltshire/laravel-model-meta allows you to use standard Eloquent getters and setters to add and remove metadata to your models. Any attributes that relate to a column in your database table will be handled as a standard attribute, but anything else will be added to the meta data.

# Example

```php
$test = new \App\Models\Test();

$test->forceFill(
[
'subject_id' => 1,
'level_id' => 1,
'slug' => 'old-slug',
'title' => 'Test Title',
'instructions' => 'Some instructions'
]
);

$test->save();

$test = \App\Models\Test::find(1);

//This is an actual field in the DB, so this will be set to that attribute
$test->slug = 'test-slug';

//This isn't a field in the DB, and isn't a relationship etc, so will be stored in the meta field
$test->meta_subject = 'This is a meta subjects';

$test->save();
```
This code would generate a new `Test` model and save it to the DB. Assuming that `meta_subject` is not a column in our table, the `meta_subject` will automatically be added to the metadata:

```text
root@localhost:[homestead]> SELECT id, slug, title, meta FROM tests;
+----+-----------+------------+--------------------------------------------+
| id | slug | title | meta |
+----+-----------+------------+--------------------------------------------+
| 1 | test-slug | Test Title | {"meta_subject":"This is a meta subjects"} |
+----+-----------+------------+--------------------------------------------+
```

# Installation
liam-wiltshire/laravel-model-meta is available as a composer package:
`composer require liam-wiltshire/laravel-model-meta`

Once installed, use the `\LiamWiltshire\LaravelModelMeta\Concerns\HasMeta` trait in your model.

The database table behind the model will need a meta column adding - by default the trait assumes this will be called `meta`:

```php
$table->json('meta')->nullable()->default(null);
```

If the name of your meta column is different, then add a `$metaDbField` property to your model containing the name of the field:

```php
protected $metaDbField = 'metaData';
```

# Limitations
This is a pre-release. Many cases have not been completely tested yet, and unexpected results may be returned. You have been warned!
35 changes: 35 additions & 0 deletions composer.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
{
"name": "liam-wiltshire/laravel-model-meta",
"description": "Metadata support for Eloquent",
"license": "MIT",
"authors": [
{
"name": "Liam Wiltshire",
"email": "liam@w.iltshi.re"
}
],
"require": {
"php": ">=7.1.0",
"illuminate/database": "^5.5.0"
},
"require-dev": {
"phpunit/phpunit": "^7.0.0",
"squizlabs/php_codesniffer" : "^3.0.0",
"phpunit/php-code-coverage": "^6.0.0"
},
"autoload": {
"psr-4": {
"LiamWiltshire\\LaravelModelMeta\\": "src/"
}
},
"autoload-dev": {
"psr-4": {
"LiamWiltshire\\LaravelModelMeta\\Tests\\": "tests/"
}
},
"scripts": {
"test": "phpunit",
"cs": "php-cs-fixer fix src/ --level=psr2"
},
"minimum-stability": "stable"
}
32 changes: 32 additions & 0 deletions phpunit.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
<?xml version="1.0" encoding="UTF-8"?>
<phpunit backupGlobals="false"
backupStaticAttributes="false"
bootstrap="vendor/autoload.php"
colors="true"
convertErrorsToExceptions="true"
convertNoticesToExceptions="true"
convertWarningsToExceptions="true"
processIsolation="false"
stopOnFailure="false">
<testsuites>
<testsuite name="Application Test Suite">
<directory suffix="Test.php">./tests</directory>
</testsuite>
</testsuites>
<logging>
<log type="coverage-clover" target="./tests/clover.xml"/>
<log type="coverage-html" target="./tests/reports" />
</logging>
<filter>
<whitelist processUncoveredFilesFromWhitelist="true">
<directory suffix=".php">./src</directory>
</whitelist>
</filter>
<php>
<env name="APP_ENV" value="testing"/>
<env name="CACHE_DRIVER" value="array"/>
<env name="SESSION_DRIVER" value="array"/>
<env name="QUEUE_DRIVER" value="sync"/>
<env name="APP_KEY" value="base64:000000000000b5EGx4/00000/0000000000000000f4="/>
</php>
</phpunit>
135 changes: 135 additions & 0 deletions src/Concerns/HasMeta.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,135 @@
<?php

namespace LiamWiltshire\LaravelModelMeta\Concerns;

use Illuminate\Database\Eloquent\Model;
use Illuminate\Support\Collection;

trait HasMeta
{

protected $tableFields;

/**
* Get all the fields for this models DB table
* @return array|null
* @codeCoverageIgnore
*/
public function getTableFields(): array
{
if (!$this->tableFields) {
$fields = new Collection($this->getConnection()->select("DESC {$this->getTable()}"));
$this->tableFields = array_flip($fields->pluck(["Field"])->toArray());
}

return $this->tableFields;
}

/**
* Override default getCasts method to add the meta field
* @return array
*/
public function getCasts(): array
{
$this->casts[$this->getMetaDbField()] = 'json';
return parent::getCasts();
}

/**
* Get the name of the column holding meta data
* @return string
*/
public function getMetaDbField(): string
{
return property_exists($this, 'metaDbField') ? $this->metaDbField : 'meta';
}

/**
* Test to see if $key should be considered an attribute (relationship, method, property or attribute)
* @param $key
* @return bool
*/
public function handleAsAttribute(string $key): bool
{
if (property_exists($this, $key)) {
return true;
}

if (parent::getAttribute($key) !== null) {
return true;
}

if (array_key_exists($key, $this->getTableFields())) {
return true;
}

if (method_exists($this, $key)) {
return true;
}

return false;
}

/**
* Get the current metadata for this model
* @return object
*/
public function getMetaData(): \stdClass
{
if (!$metaData = parent::getAttribute($this->getMetaDbField())) {
$metaData = new \stdClass();
}

return (object) $metaData;
}

/**
* Override default getAttribute method to include metadata step
* @param $key
* @return mixed|null
*/
public function getAttribute($key)
{
if ($this->handleAsAttribute($key)) {
return parent::getAttribute($key);
}

$meta = $this->getMetaData();
return $meta->{$key} ?? null;
}

/**
* Overrride default setAttribute method to include metadata step
* @param $key
* @param $value
* @return $this
* @throws \Exception
*/
public function setAttribute($key, $value) :Model
{
if ($key == $this->getMetaDbField()) {
throw new \Exception("Field {$key} shouldn't be manipulated directly");
}

if ($this->handleAsAttribute($key)) {
parent::setAttribute($key, $value);
return $this;
}

$meta = $this->getMetaData();

$meta->{$key} = $value;

parent::setAttribute($this->getMetaDbField(), $meta);
return $this;
}

public function save(array $options = [])
{
if (!parent::getAttribute($this->getMetaDbField())) {
parent::setAttribute($this->getMetaDbField(), []);
}

parent::save($options);
}
}
36 changes: 36 additions & 0 deletions tests/AltTraitModel.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
<?php

namespace LiamWiltshire\LaravelModelMeta\Tests;


use Illuminate\Database\Eloquent\Model;
use LiamWiltshire\LaravelModelMeta\Concerns\HasMeta;

class AltTraitModel extends Model {

use HasMeta;

public $table = "trait_models";
protected $metaDbField = 'metaData';
protected $fillable = ['trait_model_id', 'title'];

public $status = 9;

//SQLLite doesn't have DESCRIBE, so....
public function getTableFields()
{
return [
'id' => 0,
'trait_model_id' => 1,
'title' => 2,
'meta' => 3,
'created_at' => 4,
'updated_at' => 5
];
}

public function myRelationship()
{
return $this->hasOne(AltTraitModel::class);
}
}

0 comments on commit a4af637

Please sign in to comment.