mirror of
https://github.com/archtechx/virtualcolumn.git
synced 2025-12-12 04:04:04 +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