mirror of
https://github.com/archtechx/virtualcolumn.git
synced 2025-12-12 06:44:05 +00:00
Initial commit
This commit is contained in:
commit
1418a51fef
11 changed files with 6071 additions and 0 deletions
10
.gitattributes
vendored
Normal file
10
.gitattributes
vendored
Normal file
|
|
@ -0,0 +1,10 @@
|
||||||
|
# Path-based git attributes
|
||||||
|
# https://www.kernel.org/pub/software/scm/git/docs/gitattributes.html
|
||||||
|
|
||||||
|
# Ignore all test and documentation with "export-ignore".
|
||||||
|
/.github export-ignore
|
||||||
|
/tests export-ignore
|
||||||
|
/.gitattributes export-ignore
|
||||||
|
/.gitignore export-ignore
|
||||||
|
/phpunit.xml export-ignore
|
||||||
|
/.editorconfig export-ignore
|
||||||
16
.github/workflows/ci.yml
vendored
Normal file
16
.github/workflows/ci.yml
vendored
Normal file
|
|
@ -0,0 +1,16 @@
|
||||||
|
name: CI
|
||||||
|
|
||||||
|
on: [ push, pull_request ]
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
tests:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v2
|
||||||
|
- name: Install dependencies
|
||||||
|
run: composer install
|
||||||
|
- name: Start Redis
|
||||||
|
uses: supercharge/redis-github-action@1.1.0
|
||||||
|
- name: Run tests
|
||||||
|
run: phpunit
|
||||||
1
.gitignore
vendored
Normal file
1
.gitignore
vendored
Normal file
|
|
@ -0,0 +1 @@
|
||||||
|
/vendor/
|
||||||
21
LICENSE
Normal file
21
LICENSE
Normal file
|
|
@ -0,0 +1,21 @@
|
||||||
|
MIT License
|
||||||
|
|
||||||
|
Copyright (c) 2020 Samuel Štancl
|
||||||
|
|
||||||
|
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.
|
||||||
46
README.md
Normal file
46
README.md
Normal file
|
|
@ -0,0 +1,46 @@
|
||||||
|
# Eloquent Virtual Column
|
||||||
|
|
||||||
|
## Installation
|
||||||
|
|
||||||
|
Supports Laravel 6 and 7.
|
||||||
|
|
||||||
|
```
|
||||||
|
composer require stancl/virtual-column
|
||||||
|
```
|
||||||
|
|
||||||
|
## Usage
|
||||||
|
|
||||||
|
Use the `VirtualColumn` model on your model:
|
||||||
|
```php
|
||||||
|
use Illuminate\Database\Eloquent\Model;
|
||||||
|
use Stancl\VirtualColumn\VirtualColumn;
|
||||||
|
|
||||||
|
class MyModel extends Model
|
||||||
|
{
|
||||||
|
use VirtualColumn;
|
||||||
|
|
||||||
|
public $guarded = [];
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Create a migration:
|
||||||
|
```php
|
||||||
|
public function up()
|
||||||
|
{
|
||||||
|
Schema::create('my_models', function (Blueprint $table) {
|
||||||
|
$table->increments('id');
|
||||||
|
|
||||||
|
$table->string('custom1')->nullable();
|
||||||
|
$table->string('custom2')->nullable();
|
||||||
|
|
||||||
|
$table->json('data');
|
||||||
|
});
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
And store any data on your model:
|
||||||
|
|
||||||
|
```php
|
||||||
|
$myModel = MyModel::create(['foo' => 'bar']);
|
||||||
|
$myModel->update(['foo' => 'baz']);
|
||||||
|
```
|
||||||
28
composer.json
Normal file
28
composer.json
Normal file
|
|
@ -0,0 +1,28 @@
|
||||||
|
{
|
||||||
|
"name": "stancl/virtualcolumn",
|
||||||
|
"description": "Eloquent virtual column.",
|
||||||
|
"license": "MIT",
|
||||||
|
"authors": [
|
||||||
|
{
|
||||||
|
"name": "Samuel Štancl",
|
||||||
|
"email": "samuel.stancl@gmail.com"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"autoload": {
|
||||||
|
"psr-4": {
|
||||||
|
"Stancl\\VirtualColumn\\": "src/"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"autoload-dev": {
|
||||||
|
"psr-4": {
|
||||||
|
"Stancl\\VirtualColumn\\Tests\\": "tests/"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"require": {
|
||||||
|
"illuminate/support": "^6.0|^7.11",
|
||||||
|
"illuminate/database": "^6.0|^7.11"
|
||||||
|
},
|
||||||
|
"require-dev": {
|
||||||
|
"orchestra/testbench": "^4.0|^5.2"
|
||||||
|
}
|
||||||
|
}
|
||||||
5636
composer.lock
generated
Normal file
5636
composer.lock
generated
Normal file
File diff suppressed because it is too large
Load diff
35
phpunit.xml
Normal file
35
phpunit.xml
Normal file
|
|
@ -0,0 +1,35 @@
|
||||||
|
<?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="Unit">
|
||||||
|
<directory suffix="Test.php">./tests</directory>
|
||||||
|
</testsuite>
|
||||||
|
</testsuites>
|
||||||
|
<filter>
|
||||||
|
<whitelist processUncoveredFilesFromWhitelist="true">
|
||||||
|
<directory suffix=".php">./src</directory>
|
||||||
|
<exclude>
|
||||||
|
<file>./src/routes.php</file>
|
||||||
|
</exclude>
|
||||||
|
</whitelist>
|
||||||
|
</filter>
|
||||||
|
<php>
|
||||||
|
<env name="APP_ENV" value="testing"/>
|
||||||
|
<env name="BCRYPT_ROUNDS" value="4"/>
|
||||||
|
<env name="CACHE_DRIVER" value="redis"/>
|
||||||
|
<env name="MAIL_DRIVER" value="array"/>
|
||||||
|
<env name="QUEUE_CONNECTION" value="sync"/>
|
||||||
|
<env name="SESSION_DRIVER" value="array"/>
|
||||||
|
<env name="DB_CONNECTION" value="sqlite"/>
|
||||||
|
<env name="DB_DATABASE" value=":memory:"/>
|
||||||
|
<env name="AWS_DEFAULT_REGION" value="us-west-2"/>
|
||||||
|
</php>
|
||||||
|
</phpunit>
|
||||||
140
src/VirtualColumn.php
Normal file
140
src/VirtualColumn.php
Normal file
|
|
@ -0,0 +1,140 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace Stancl\VirtualColumn;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This trait lets you add a "data" column functionality to any Eloquent model.
|
||||||
|
* It serializes attributes which don't exist as columns on the model's table
|
||||||
|
* into a JSON column named data (customizable by overriding getDataColumn).
|
||||||
|
*/
|
||||||
|
trait VirtualColumn
|
||||||
|
{
|
||||||
|
public static $afterListeners = [];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* We need this property, because both created & saved event listeners
|
||||||
|
* decode the data (to take precedence before other created & saved)
|
||||||
|
* listeners, but we don't want the dadta to be decoded twice.
|
||||||
|
*
|
||||||
|
* @var string
|
||||||
|
*/
|
||||||
|
public $dataEncodingStatus = 'decoded';
|
||||||
|
|
||||||
|
protected static function decodeVirtualColumn(self $model): void
|
||||||
|
{
|
||||||
|
if ($model->dataEncodingStatus === 'decoded') {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach ($model->getAttribute(static::getDataColumn()) ?? [] as $key => $value) {
|
||||||
|
$model->setAttribute($key, $value);
|
||||||
|
}
|
||||||
|
|
||||||
|
$model->setAttribute(static::getDataColumn(), null);
|
||||||
|
|
||||||
|
$model->dataEncodingStatus = 'decoded';
|
||||||
|
}
|
||||||
|
|
||||||
|
protected static function encodeAttributes(self $model): void
|
||||||
|
{
|
||||||
|
if ($model->dataEncodingStatus === 'encoded') {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach ($model->getAttributes() as $key => $value) {
|
||||||
|
if (! in_array($key, static::getCustomColumns())) {
|
||||||
|
$current = $model->getAttribute(static::getDataColumn()) ?? [];
|
||||||
|
|
||||||
|
$model->setAttribute(static::getDataColumn(), array_merge($current, [
|
||||||
|
$key => $value,
|
||||||
|
]));
|
||||||
|
|
||||||
|
unset($model->attributes[$key]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$model->dataEncodingStatus = 'encoded';
|
||||||
|
}
|
||||||
|
|
||||||
|
public static function bootVirtualColumn()
|
||||||
|
{
|
||||||
|
static::registerAfterListener('retrieved', function ($model) {
|
||||||
|
// We always decode after model retrieval.
|
||||||
|
$model->dataEncodingStatus = 'encoded';
|
||||||
|
|
||||||
|
static::decodeVirtualColumn($model);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Encode if writing
|
||||||
|
static::registerAfterListener('saving', [static::class, 'encodeAttributes']);
|
||||||
|
static::registerAfterListener('creating', [static::class, 'encodeAttributes']);
|
||||||
|
static::registerAfterListener('updating', [static::class, 'encodeAttributes']);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected function decodeIfEncoded()
|
||||||
|
{
|
||||||
|
if ($this->dataEncodingStatus === 'encoded') {
|
||||||
|
static::decodeVirtualColumn($this);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
protected function fireModelEvent($event, $halt = true)
|
||||||
|
{
|
||||||
|
$this->decodeIfEncoded();
|
||||||
|
|
||||||
|
$result = parent::fireModelEvent($event, $halt);
|
||||||
|
|
||||||
|
$this->runAfterListeners($event, $halt);
|
||||||
|
|
||||||
|
return $result;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function runAfterListeners($event, $halt = true)
|
||||||
|
{
|
||||||
|
$listeners = static::$afterListeners[$event] ?? [];
|
||||||
|
|
||||||
|
if (! $event) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach ($listeners as $listener) {
|
||||||
|
if (is_string($listener)) {
|
||||||
|
$listener = app($listener);
|
||||||
|
$handle = [$listener, 'handle'];
|
||||||
|
} else {
|
||||||
|
$handle = $listener;
|
||||||
|
}
|
||||||
|
|
||||||
|
$handle($this);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static function registerAfterListener(string $event, callable $callback)
|
||||||
|
{
|
||||||
|
static::$afterListeners[$event][] = $callback;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getCasts()
|
||||||
|
{
|
||||||
|
return array_merge(parent::getCasts(), [
|
||||||
|
static::getDataColumn() => 'array',
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the name of the column that stores additional data.
|
||||||
|
*/
|
||||||
|
public static function getDataColumn(): string
|
||||||
|
{
|
||||||
|
return 'data';
|
||||||
|
}
|
||||||
|
|
||||||
|
public static function getCustomColumns(): array
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
'id',
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
||||||
106
tests/VirtualColumnTest.php
Normal file
106
tests/VirtualColumnTest.php
Normal file
|
|
@ -0,0 +1,106 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace Stancl\VirtualColumn\Tests;
|
||||||
|
|
||||||
|
use Illuminate\Database\Eloquent\Model;
|
||||||
|
use Illuminate\Support\Facades\DB;
|
||||||
|
use Orchestra\Testbench\TestCase;
|
||||||
|
use Stancl\VirtualColumn\VirtualColumn;
|
||||||
|
|
||||||
|
class VirtualColumnTest extends TestCase
|
||||||
|
{
|
||||||
|
public function setUp(): void
|
||||||
|
{
|
||||||
|
parent::setUp();
|
||||||
|
|
||||||
|
$this->loadMigrationsFrom(__DIR__ . '/etc/migrations');
|
||||||
|
}
|
||||||
|
|
||||||
|
/** @test */
|
||||||
|
public function keys_which_dont_have_their_own_column_go_into_data_json_column()
|
||||||
|
{
|
||||||
|
$model = MyModel::create([
|
||||||
|
'foo' => 'bar',
|
||||||
|
]);
|
||||||
|
|
||||||
|
// Test that model works correctly
|
||||||
|
$this->assertSame('bar', $model->foo);
|
||||||
|
$this->assertSame(null, $model->data);
|
||||||
|
|
||||||
|
// Low level test to assert database structure
|
||||||
|
$this->assertSame(['foo' => 'bar'], json_decode(DB::table('my_models')->where('id', $model->id)->first()->data, true));
|
||||||
|
$this->assertSame(null, DB::table('my_models')->where('id', $model->id)->first()->foo ?? null);
|
||||||
|
|
||||||
|
// Model has the correct structure when retrieved
|
||||||
|
$model = MyModel::first();
|
||||||
|
$this->assertSame('bar', $model->foo);
|
||||||
|
$this->assertSame(null, $model->data);
|
||||||
|
|
||||||
|
// Model can be updated
|
||||||
|
$model->update([
|
||||||
|
'foo' => 'baz',
|
||||||
|
'abc' => 'xyz',
|
||||||
|
]);
|
||||||
|
|
||||||
|
$this->assertSame('baz', $model->foo);
|
||||||
|
$this->assertSame('xyz', $model->abc);
|
||||||
|
$this->assertSame(null, $model->data);
|
||||||
|
|
||||||
|
// Model can be retrieved after update & is structure correctly
|
||||||
|
$model = MyModel::first();
|
||||||
|
|
||||||
|
$this->assertSame('baz', $model->foo);
|
||||||
|
$this->assertSame('xyz', $model->abc);
|
||||||
|
$this->assertSame(null, $model->data);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** @test */
|
||||||
|
public function model_is_always_decoded_when_accessed_by_user_event()
|
||||||
|
{
|
||||||
|
MyModel::retrieved(function (MyModel $model) {
|
||||||
|
$this->assertSame('decoded', $model->dataEncodingStatus);
|
||||||
|
});
|
||||||
|
MyModel::saving(function (MyModel $model) {
|
||||||
|
$this->assertSame('decoded', $model->dataEncodingStatus);
|
||||||
|
});
|
||||||
|
MyModel::updating(function (MyModel $model) {
|
||||||
|
$this->assertSame('decoded', $model->dataEncodingStatus);
|
||||||
|
});
|
||||||
|
MyModel::creating(function (MyModel $model) {
|
||||||
|
$this->assertSame('decoded', $model->dataEncodingStatus);
|
||||||
|
});
|
||||||
|
MyModel::saved(function (MyModel $model) {
|
||||||
|
$this->assertSame('decoded', $model->dataEncodingStatus);
|
||||||
|
});
|
||||||
|
MyModel::updated(function (MyModel $model) {
|
||||||
|
$this->assertSame('decoded', $model->dataEncodingStatus);
|
||||||
|
});
|
||||||
|
MyModel::created(function (MyModel $model) {
|
||||||
|
$this->assertSame('decoded', $model->dataEncodingStatus);
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
$model = MyModel::create(['foo' => 'bar']);
|
||||||
|
$model->update(['foo' => 'baz']);
|
||||||
|
MyModel::first();
|
||||||
|
}
|
||||||
|
|
||||||
|
// maybe add an explicit test that the saving() and updating() listeners don't run twice?
|
||||||
|
}
|
||||||
|
|
||||||
|
class MyModel extends Model
|
||||||
|
{
|
||||||
|
use VirtualColumn;
|
||||||
|
|
||||||
|
protected $guarded = [];
|
||||||
|
public $timestamps = false;
|
||||||
|
|
||||||
|
public static function getCustomColumns(): array
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
'id',
|
||||||
|
'custom1',
|
||||||
|
'custom2',
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,32 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
use Illuminate\Database\Migrations\Migration;
|
||||||
|
use Illuminate\Database\Schema\Blueprint;
|
||||||
|
use Illuminate\Support\Facades\Schema;
|
||||||
|
|
||||||
|
class CreateMyModelsTable extends Migration
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Run the migrations.
|
||||||
|
*
|
||||||
|
* @return void
|
||||||
|
*/
|
||||||
|
public function up()
|
||||||
|
{
|
||||||
|
Schema::create('my_models', function (Blueprint $table) {
|
||||||
|
$table->increments('id');
|
||||||
|
|
||||||
|
$table->string('custom1')->nullable();
|
||||||
|
$table->string('custom2')->nullable();
|
||||||
|
|
||||||
|
$table->json('data');
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
public function down()
|
||||||
|
{
|
||||||
|
Schema::dropIfExists('my_models');
|
||||||
|
}
|
||||||
|
}
|
||||||
Loading…
Add table
Add a link
Reference in a new issue