├── .gitignore
├── LICENSE
├── README.md
├── composer.json
├── phpunit.xml
├── src
├── Builder.php
├── BuilderTrait.php
├── SoftDeletes.php
├── Versionable.php
└── VersioningScope.php
└── tests
├── Models
├── Post.php
└── User.php
├── TestCase.php
├── Unit
├── BuilderTest.php
├── SoftDeletesTest.php
└── VersionableTest.php
└── database
├── factories
├── PostFactory.php
└── UserFactory.php
└── migrations
├── 2014_10_12_000000_create_users_table.php
└── 2014_10_12_100000_create_posts_table.php
/.gitignore:
--------------------------------------------------------------------------------
1 | .idea
2 | /vendor
3 | /coverage
4 | composer.lock
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | The MIT License (MIT)
2 |
3 | Copyright (c) 2015-present Markus Wetzel
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
23 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Eloquent Versioning
2 |
3 | [](https://packagist.org/packages/proai/eloquent-versioning) [](https://packagist.org/packages/proai/eloquent-versioning) [](https://packagist.org/packages/proai/eloquent-versioning) [](https://packagist.org/packages/proai/eloquent-versioning)
4 |
5 | This is an extension for the Eloquent ORM to support versioning. You can specify attributes as versioned. If an attribute is specified as versioned the value will be saved in a separate version table on each update. It is possible to use timestamps and soft deletes with this feature.
6 |
7 | ## Installation
8 |
9 | Eloquent Versioning is distributed as a composer package. So you first have to add the package to your `composer.json` file:
10 |
11 | ```
12 | "proai/eloquent-versioning": "~1.0"
13 | ```
14 |
15 | Then you have to run `composer update` to install the package.
16 |
17 | ## Example
18 |
19 | We assume that we want a simple user model. While the username should be fixed, the email and city should be versionable. Also timestamps and soft deletes should be versioned. The migrations would look like the following:
20 |
21 | ```php
22 | ...
23 |
24 | Schema::create('users', function(Blueprint $table) {
25 | $table->increments('id');
26 | $table->integer('latest_version');
27 | $table->string('username');
28 | $table->timestamp('created_at');
29 | });
30 |
31 | Schema::create('users_version', function(Blueprint $table) {
32 | $table->integer('ref_id')->primary();
33 | $table->integer('version')->primary();
34 | $table->string('email');
35 | $table->string('city');
36 | $table->timestamp('updated_at');
37 | $table->timestamp('deleted_at');
38 | });
39 |
40 | ...
41 | ```
42 |
43 | The referring Eloquent model should include the code below:
44 |
45 | ```php
46 | Example: `User::version(2)->find(1)` will return version #2 of user #1
90 |
91 | * `allVersions()` returns all versions of the queried items
Example: `User::allVersions()->get()` will return all versions of all users
92 |
93 | * `moment(Carbon)` returns a specific version, closest but lower than the input date
Example: `User::moment(Carbon::now()->subWeek()->find(1)` will return the version at that point in time.
94 |
95 | #### Create, update and delete records
96 |
97 | All these operations can be performed normally. The package will automatically generate a version 1 on create, the next version on update and will remove all versions on delete.
98 |
99 | ### Timestamps
100 |
101 | You can use timestamps in two ways. For both you have to set `$timestamps = true;`.
102 |
103 | * Normal timestamps
The main table must include a `created_at` and a `updated_at` column. The `updated_at` column will be overriden on every update. So this is the normal use of Eloquent timestamps.
104 |
105 | * Versioned timestamps
If you add `updated_at` to your `$versioned` array, you need a `created_at` column in the main table and a `updated_at` column in the version table (see example). On update the `updated_at` value of the new version will be set to the current time. The `updated_at` values of previous versions will not be updated. This way you can track the dates of all updates.
106 |
107 | ### Soft Deletes
108 |
109 | If you use the `Versionable` trait with soft deletes, you have to use the `ProAI\Versioning\SoftDeletes` trait **from this package** instead of the Eloquent soft deletes trait.
110 |
111 | * Normal soft deletes
Just use a `deleted_at` column in the main table. Then on delete or on restore the `deleted_at` value will be updated.
112 |
113 | * Versioned soft deletes
If you create a `deleted_at` column in the version table and add `deleted_at` to the `$versioned` array, then on delete or on restore the `deleted_at` value of the new version will get updated (see example). The `deleted_at` values of previous versions will not be updated. This way you can track all soft deletes and restores.
114 |
115 | ## Custom Query Builder
116 |
117 | If you want to use a custom versioning query builder, you will have to build your own versioning trait, but that's pretty easy:
118 |
119 | ```php
120 | =7.1",
25 | "illuminate/database": "5.*"
26 | },
27 | "require-dev": {
28 | "phpunit/phpunit": "^7.0",
29 | "mockery/mockery": "^1.0",
30 | "orchestra/testbench": "^3.6",
31 | "orchestra/database": "^3.6"
32 | },
33 | "autoload": {
34 | "psr-4": {
35 | "ProAI\\Versioning\\": "src/"
36 | }
37 | },
38 | "autoload-dev": {
39 | "psr-4": {
40 | "ProAI\\Versioning\\Tests\\": "tests/"
41 | }
42 | },
43 | "extra": {
44 | "branch-alias": {
45 | "dev-master": "1.0-dev"
46 | }
47 | }
48 | }
49 |
--------------------------------------------------------------------------------
/phpunit.xml:
--------------------------------------------------------------------------------
1 |
2 |
15 |
16 |
17 | ./tests
18 |
19 |
20 |
21 |
22 | ./src
23 |
24 |
25 |
--------------------------------------------------------------------------------
/src/Builder.php:
--------------------------------------------------------------------------------
1 | query->columns)
19 | ? array_merge($columns, $this->query->columns)
20 | : $columns;
21 | foreach($tempColumns as $column) {
22 | $segments = explode('.', $column);
23 | if ($segments[0] == $this->model->getTable()) {
24 | $this->query->addSelect($this->model->getVersionTable().'.*');
25 | break;
26 | }
27 | }
28 |
29 | return parent::getModels($columns);
30 | }
31 |
32 | /**
33 | * Insert a new record into the database.
34 | *
35 | * @param array $values
36 | * @return bool
37 | */
38 | public function insert(array $values)
39 | {
40 | // get version values & values
41 | $versionValues = $this->getVersionValues($values);
42 | $values = $this->getValues($values);
43 |
44 | // set version, ref_id and latest_version
45 | $values[$this->model->getLatestVersionColumn()] = 1;
46 |
47 | // insert main table record
48 | if (! $id = $this->query->insertGetId($values)) {
49 | return false;
50 | }
51 |
52 | $versionValues[$this->model->getVersionKeyName()] = $id;
53 | $versionValues[$this->model->getVersionColumn()] = 1;
54 |
55 | // insert version table record
56 | $db = $this->model->getConnection();
57 | return $db->table($this->model->getVersionTable())->insert($versionValues);
58 | }
59 |
60 | /**
61 | * Insert a new record and get the value of the primary key.
62 | *
63 | * @param array $values
64 | * @param string $sequence
65 | * @return int
66 | */
67 | public function insertGetId(array $values, $sequence = null)
68 | {
69 | // get version values & values
70 | $versionValues = $this->getVersionValues($values);
71 | $values = $this->getValues($values);
72 |
73 | // set version and latest_version
74 | $values[$this->model->getLatestVersionColumn()] = 1;
75 | $versionValues[$this->model->getVersionColumn()] = 1;
76 |
77 | // insert main table record
78 | if (! $id = $this->query->insertGetId($values, $sequence)) {
79 | return false;
80 | }
81 |
82 | // set ref_id
83 | $versionValues[$this->model->getVersionKeyName()] = $id;
84 |
85 | // insert version table record
86 | $db = $this->model->getConnection();
87 | if (! $db->table($this->model->getVersionTable())->insert($versionValues)) {
88 | return false;
89 | }
90 |
91 | // fill the latest version value
92 | $this->model->{$this->model->getLatestVersionColumn()} = 1;
93 |
94 | return $id;
95 | }
96 |
97 | /**
98 | * Update a record in the database.
99 | *
100 | * @param array $values
101 | * @return int
102 | */
103 | public function update(array $values)
104 | {
105 | // update timestamps
106 | $values = $this->addUpdatedAtColumn($values);
107 |
108 | // get version values & values
109 | $versionValues = $this->getVersionValues($values);
110 | $values = $this->getValues($values);
111 |
112 | // get records
113 | $affectedRecords = $this->getAffectedRecords();
114 |
115 | // update main table records
116 | if (! $this->query->increment($this->model->getLatestVersionColumn(), 1, $values)) {
117 | return false;
118 | }
119 |
120 | // update version table records
121 | $db = $this->model->getConnection();
122 | foreach ($affectedRecords as $record) {
123 | // get versioned values from record
124 | foreach($this->model->getVersionedAttributeNames() as $key) {
125 | $recordVersionValues[$key] = (isset($versionValues[$key])) ? $versionValues[$key] : $record->{$key};
126 | }
127 |
128 | // merge versioned values from record and input
129 | $recordVersionValues = array_merge($recordVersionValues, $versionValues);
130 |
131 | // set version and ref_id
132 | $recordVersionValues[$this->model->getVersionKeyName()] = $record->{$this->model->getKeyName()};
133 | $recordVersionValues[$this->model->getVersionColumn()] = $record->{$this->model->getLatestVersionColumn()}+1;
134 |
135 | // insert new version
136 | if(! $db->table($this->model->getVersionTable())->insert($recordVersionValues)) {
137 | return false;
138 | }
139 | }
140 |
141 | // fill the latest version value
142 | $this->model->{$this->model->getLatestVersionColumn()} += 1;
143 |
144 | return true;
145 | }
146 |
147 | /**
148 | * Delete a record from the database.
149 | *
150 | * @return mixed
151 | */
152 | public function delete()
153 | {
154 | if (isset($this->onDelete)) {
155 | return call_user_func($this->onDelete, $this);
156 | }
157 |
158 | $this->forceDelete();
159 | }
160 |
161 | /**
162 | * Run the default delete function on the builder.
163 | *
164 | * @return mixed
165 | */
166 | public function forceDelete()
167 | {
168 | // get records
169 | $affectedRecords = $this->getAffectedRecords()->toArray();
170 | $ids = array_map(function($record) {
171 | return $record->{$this->model->getKeyName()};
172 | }, $affectedRecords);
173 |
174 | // delete main table records
175 | if (! $this->query->delete()) {
176 | return false;
177 | }
178 |
179 | // delete version table records
180 | $db = $this->model->getConnection();
181 | return $db->table($this->model->getVersionTable())
182 | ->whereIn($this->model->getVersionKeyName(), $ids)
183 | ->delete();
184 | }
185 |
186 | /**
187 | * Get affected records.
188 | *
189 | * @return array
190 | */
191 | protected function getAffectedRecords()
192 | {
193 | // model only
194 | if ($this->model->getKey()) {
195 | $records = [$this->model];
196 | }
197 |
198 | // mass assignment
199 | else {
200 | $records = $this->query->get();
201 | }
202 |
203 | return $records;
204 | }
205 |
206 | /**
207 | * Get affected ids.
208 | *
209 | * @param array $values
210 | * @return array
211 | */
212 | protected function getValues(array $values)
213 | {
214 | $array = [];
215 |
216 | $versionedKeys = array_merge(
217 | $this->model->getVersionedAttributeNames(),
218 | [$this->model->getLatestVersionColumn(), $this->model->getVersionColumn(), $this->model->getVersionKeyName()]
219 | );
220 |
221 | foreach ($values as $key => $value) {
222 | if (! $this->isVersionedKey($key, $versionedKeys)) {
223 | $array[$key] = $value;
224 | }
225 | }
226 |
227 | return $array;
228 | }
229 |
230 | /**
231 | * Get affected ids.
232 | *
233 | * @param array $values
234 | * @return array
235 | */
236 | protected function getVersionValues(array $values)
237 | {
238 | $array = [];
239 |
240 | $versionedKeys = $this->model->getVersionedAttributeNames();
241 |
242 | foreach ($values as $key => $value) {
243 | if ($newKey = $this->isVersionedKey($key, $versionedKeys)) {
244 | $array[$newKey] = $value;
245 | }
246 | }
247 |
248 | return $array;
249 | }
250 |
251 | /**
252 | * Check if key is in versioned keys.
253 | *
254 | * @param string $key
255 | * @param array $versionedKeys
256 | * @return string|null
257 | */
258 | protected function isVersionedKey($key, array $versionedKeys)
259 | {
260 | $segments = explode(".",$key);
261 |
262 | if (count($segments) > 2) {
263 | throw new Exception("Key '".$key."' has too many fractions.");
264 | }
265 |
266 | if (count($segments) == 1 && in_array($segments[0], $versionedKeys)) {
267 | return $segments[0];
268 | }
269 |
270 | if (count($segments) == 2 && $segments[0] == $this->model->getVersionTable() && in_array($segments[1], $versionedKeys)) {
271 | return $segments[1];
272 | }
273 |
274 | return null;
275 | }
276 |
277 | }
278 |
--------------------------------------------------------------------------------
/src/SoftDeletes.php:
--------------------------------------------------------------------------------
1 | getDeletedAtColumn();
19 |
20 | if (isset($this->versioned) && in_array($deletedAt, $this->versioned)) {
21 | return $this->getVersionTable().'.'.$deletedAt;
22 | }
23 |
24 | return $this->getTable().'.'.$deletedAt;
25 | }
26 | }
27 |
--------------------------------------------------------------------------------
/src/Versionable.php:
--------------------------------------------------------------------------------
1 | getVersionKeyName());
28 |
29 | return parent::newFromBuilder($attributes, $connection);
30 | }
31 |
32 | /**
33 | * Create a new Eloquent query builder for the model.
34 | *
35 | * @param \Illuminate\Database\Query\Builder $query
36 | * @return \ProAI\Versioning\Builder|static
37 | */
38 | public function newEloquentBuilder($query)
39 | {
40 | return new Builder($query);
41 | }
42 |
43 | /**
44 | * Get the names of the attributes that are versioned.
45 | *
46 | * @return array
47 | */
48 | public function getVersionedAttributeNames()
49 | {
50 | return (! empty($this->versioned)) ? $this->versioned : [];
51 | }
52 |
53 | /**
54 | * Get the version key name.
55 | *
56 | * @return string
57 | */
58 | public function getVersionKeyName()
59 | {
60 | return 'ref_' . $this->getKeyName();
61 | }
62 |
63 | /**
64 | * Get the version table associated with the model.
65 | *
66 | * @return string
67 | */
68 | public function getVersionTable()
69 | {
70 | return $this->getTable() . '_version';
71 | }
72 |
73 | /**
74 | * Get the table qualified version key name.
75 | *
76 | * @return string
77 | */
78 | public function getQualifiedVersionKeyName()
79 | {
80 | return $this->getVersionTable().'.'.$this->getVersionKeyName();
81 | }
82 |
83 | /**
84 | * Get the name of the "latest version" column.
85 | *
86 | * @return string
87 | */
88 | public function getLatestVersionColumn()
89 | {
90 | return defined('static::LATEST_VERSION') ? static::LATEST_VERSION : 'latest_version';
91 | }
92 |
93 | /**
94 | * Get the fully qualified "latest version" column.
95 | *
96 | * @return string
97 | */
98 | public function getQualifiedLatestVersionColumn()
99 | {
100 | return $this->getTable().'.'.$this->getLatestVersionColumn();
101 | }
102 |
103 | /**
104 | * Get the name of the "version" column.
105 | *
106 | * @return string
107 | */
108 | public function getVersionColumn()
109 | {
110 | return defined('static::VERSION') ? static::VERSION : 'version';
111 | }
112 |
113 | /**
114 | * Get the fully qualified "version" column.
115 | *
116 | * @return string
117 | */
118 | public function getQualifiedVersionColumn()
119 | {
120 | return $this->getVersionTable().'.'.$this->getVersionColumn();
121 | }
122 |
123 | }
124 |
--------------------------------------------------------------------------------
/src/VersioningScope.php:
--------------------------------------------------------------------------------
1 | hasVersionJoin($builder, $model->getVersionTable())) {
30 | $builder->join($model->getVersionTable(), function($join) use ($model) {
31 | $join->on($model->getQualifiedKeyName(), '=', $model->getQualifiedVersionKeyName());
32 | $join->on($model->getQualifiedVersionColumn(), '=', $model->getQualifiedLatestVersionColumn());
33 | });
34 | }
35 |
36 | $this->extend($builder);
37 | }
38 |
39 | /**
40 | * Remove the scope from the given Eloquent query builder.
41 | *
42 | * @param \Illuminate\Database\Eloquent\Builder $builder
43 | * @param \Illuminate\Database\Eloquent\Model $model
44 | * @return void
45 | */
46 | public function remove(Builder $builder, Model $model)
47 | {
48 | $table = $model->getVersionTable();
49 |
50 | $query = $builder->getQuery();
51 |
52 | $query->joins = collect($query->joins)->reject(function($join) use ($table)
53 | {
54 | return $this->isVersionJoinConstraint($join, $table);
55 | })->values()->all();
56 | }
57 |
58 | /**
59 | * Extend the query builder with the needed functions.
60 | *
61 | * @param \Illuminate\Database\Eloquent\Builder $builder
62 | * @return void
63 | */
64 | public function extend(Builder $builder)
65 | {
66 | foreach ($this->extensions as $extension)
67 | {
68 | $this->{"add{$extension}"}($builder);
69 | }
70 | }
71 |
72 | /**
73 | * Add the version extension to the builder.
74 | *
75 | * @param \Illuminate\Database\Eloquent\Builder $builder
76 | * @return void
77 | */
78 | protected function addVersion(Builder $builder)
79 | {
80 | $builder->macro('version', function(Builder $builder, $version) {
81 | $model = $builder->getModel();
82 |
83 | $this->remove($builder, $builder->getModel());
84 |
85 | $builder->join($model->getVersionTable(), function($join) use ($model, $version) {
86 | $join->on($model->getQualifiedKeyName(), '=', $model->getQualifiedVersionKeyName());
87 | $join->where($model->getQualifiedVersionColumn(), '=', $version);
88 | });
89 |
90 | return $builder;
91 | });
92 | }
93 |
94 | /**
95 | * Add the allVersions extension to the builder.
96 | *
97 | * @param \Illuminate\Database\Eloquent\Builder $builder
98 | * @return void
99 | */
100 | protected function addAllVersions(Builder $builder)
101 | {
102 | $builder->macro('allVersions', function(Builder $builder) {
103 | $model = $builder->getModel();
104 |
105 | $this->remove($builder, $builder->getModel());
106 |
107 | $builder->join($model->getVersionTable(), function($join) use ($model) {
108 | $join->on($model->getQualifiedKeyName(), '=', $model->getQualifiedVersionKeyName());
109 | });
110 |
111 | return $builder;
112 | });
113 | }
114 |
115 | /**
116 | * Add the moment extension to the builder.
117 | *
118 | * @param \Illuminate\Database\Eloquent\Builder $builder
119 | * @return void
120 | */
121 | protected function addMoment(Builder $builder)
122 | {
123 | $builder->macro('moment', function(Builder $builder, Carbon $moment) {
124 | $model = $builder->getModel();
125 |
126 | $this->remove($builder, $builder->getModel());
127 |
128 | $builder->join($model->getVersionTable(), function($join) use ($model, $moment) {
129 | $join->on($model->getQualifiedKeyName(), '=', $model->getQualifiedVersionKeyName());
130 | $join->where('updated_at', '<=', $moment)->orderBy('updated_at', 'desc')->limit(1);
131 | })->orderBy('updated_at', 'desc')->limit(1);
132 |
133 | return $builder;
134 | });
135 | }
136 |
137 | /**
138 | * Determine if the given join clause is a version constraint.
139 | *
140 | * @param \Illuminate\Database\Query\JoinClause $join
141 | * @param string $column
142 | * @return bool
143 | */
144 | protected function isVersionJoinConstraint(JoinClause $join, $table)
145 | {
146 | return $join->type == 'inner' && $join->table == $table;
147 | }
148 |
149 | /**
150 | * Determine if the given builder contains a join with the given table
151 | *
152 | * @param Builder $builder
153 | * @param string $table
154 | * @return bool
155 | */
156 | protected function hasVersionJoin(Builder $builder, string $table)
157 | {
158 | return collect($builder->getQuery()->joins)->pluck('table')->contains($table);
159 | }
160 | }
161 |
--------------------------------------------------------------------------------
/tests/Models/Post.php:
--------------------------------------------------------------------------------
1 | set('database.default', 'testing');
17 | $app['config']->set('database.connections.testing', [
18 | 'driver' => 'sqlite',
19 | 'database' => ':memory:',
20 | 'prefix' => '',
21 | ]);
22 | }
23 |
24 | /**
25 | * {@inheritdoc}
26 | */
27 | public function setUp()
28 | {
29 | parent::setUp();
30 | $this->loadMigrationsFrom(__DIR__.'/database/migrations');
31 | $this->withFactories(__DIR__.'/database/factories');
32 | }
33 | }
--------------------------------------------------------------------------------
/tests/Unit/BuilderTest.php:
--------------------------------------------------------------------------------
1 | create([]);
19 |
20 | $this->assertArraySubset([
21 | 'username' => $model->username,
22 | 'email' => $model->email,
23 | 'city' => $model->city,
24 | 'latest_version' => $model->latest_version,
25 | 'updated_at' => $model->updated_at,
26 | 'created_at' => $model->created_at
27 | ], User::first()->toArray());
28 | }
29 |
30 | /**
31 | * @test
32 | */
33 | public function itWillRetrieveTheLatestVersionedAttributes()
34 | {
35 | /** @var User $model */
36 | $model = factory(User::class)->create([]);
37 |
38 | $model->update([
39 | 'city' => 'Citadel'
40 | ]);
41 |
42 | $this->assertArraySubset([
43 | 'latest_version' => 2,
44 | ], User::first()->toArray());
45 | }
46 |
47 | /**
48 | * @test
49 | */
50 | public function itWillRetrieveTheCorrectVersionsAttributes()
51 | {
52 | /** @var User $model */
53 | $model = factory(User::class)->create([]);
54 | $city = $model->city;
55 |
56 | $model->update([
57 | 'city' => 'Citadel'
58 | ]);
59 |
60 | $model->update([
61 | 'city' => 'Ricklantis'
62 | ]);
63 |
64 | $this->assertArraySubset([
65 | 'city' => $city,
66 | 'version' => 1
67 | ], User::version(1)->find($model->id)->toArray());
68 |
69 | $this->assertArraySubset([
70 | 'city' => 'Citadel',
71 | 'version' => 2
72 | ], User::version(2)->find($model->id)->toArray());
73 |
74 | $this->assertArraySubset([
75 | 'city' => 'Ricklantis',
76 | 'version' => 3
77 | ], User::version(3)->find($model->id)->toArray());
78 | }
79 |
80 | /**
81 | * @test
82 | */
83 | public function itWillRetrieveAllVersions()
84 | {
85 | /** @var User $model */
86 | $model = factory(User::class)->create([]);
87 | $city = $model->city;
88 |
89 | $model->update([
90 | 'city' => 'Citadel'
91 | ]);
92 |
93 | $model->update([
94 | 'city' => 'Ricklantis'
95 | ]);
96 |
97 | $this->assertArraySubset([
98 | [
99 | 'city' => $city
100 | ],
101 | [
102 | 'city' => 'Citadel'
103 | ],
104 | [
105 | 'city' => 'Ricklantis'
106 | ]
107 | ], User::allVersions()->get()->toArray());
108 | }
109 |
110 | /**
111 | * @test
112 | */
113 | public function itWillRetrieveTheCorrectMomentsAttributes()
114 | {
115 | /** @var User $model */
116 | $model = factory(User::class)->create([
117 | 'updated_at' => Carbon::now()->subDays(2)
118 | ]);
119 | $date = $model->created_at;
120 |
121 | DB::table('users_version')->insert([
122 | 'ref_id' => 1,
123 | 'version' => 2,
124 | 'email' => $model->email,
125 | 'city' => 'Citadel',
126 | 'updated_at' => $date->copy()->addDays(1)
127 | ]);
128 |
129 | DB::table('users_version')->insert([
130 | 'ref_id' => 1,
131 | 'version' => 3,
132 | 'email' => $model->email,
133 | 'city' => 'Ricklantis',
134 | 'updated_at' => $date->copy()->addDays(2)
135 | ]);
136 |
137 | $this->assertArraySubset([
138 | 'version' => 1
139 | ], User::moment($date)->find($model->id)->toArray());
140 |
141 | $this->assertArraySubset([
142 | 'version' => 2
143 | ], User::moment($date->copy()->addDays(1))->find($model->id)->toArray());
144 |
145 | $this->assertArraySubset([
146 | 'version' => 3
147 | ], User::moment($date->copy()->addDays(2))->find($model->id)->toArray());
148 | }
149 |
150 | /**
151 | * @test
152 | */
153 | public function itWillRemovePreviousJoins()
154 | {
155 | /** @var User $model */
156 | $model = factory(User::class)->create([]);
157 | $city = $model->city;
158 |
159 | $model->update([
160 | 'city' => 'Citadel'
161 | ]);
162 |
163 | $builder = User::version(1);
164 |
165 | // It should have one join right now
166 | $this->assertEquals(1, collect($builder->getQuery()->joins)->where('table', '=', 'users_version')->count());
167 |
168 | $builder->version(2);
169 |
170 | // It should still have one join right now
171 | $this->assertEquals(1, collect($builder->getQuery()->joins)->where('table', '=', 'users_version')->count());
172 | }
173 |
174 | /**
175 | * @test
176 | *
177 | * @dataProvider modelProvider
178 | * @param string $model
179 | */
180 | public function itWillDeleteTheVersionedTable(string $model)
181 | {
182 | factory($model)->create([]);
183 | factory($model)->create([]);
184 |
185 | $model::version(1)->delete();
186 |
187 | $this->assertEquals(0, User::all()->count());
188 | }
189 |
190 | /**
191 | * @test
192 | *
193 | * @dataProvider modelProvider
194 | * @param string $model
195 | */
196 | public function itWillForceDeleteTheVersionedTable(string $model)
197 | {
198 | factory($model)->create([]);
199 | factory($model)->create([]);
200 |
201 | $model::version(1)->forceDelete();
202 |
203 | $this->assertEquals(0, User::all()->count());
204 | }
205 |
206 | public function modelProvider()
207 | {
208 | return [
209 | [
210 | User::class
211 | ],
212 | [
213 | Post::class
214 | ]
215 | ];
216 | }
217 | }
218 |
--------------------------------------------------------------------------------
/tests/Unit/SoftDeletesTest.php:
--------------------------------------------------------------------------------
1 | create([]);
22 | $model->delete();
23 |
24 | $this->assertArraySubset([
25 | 'id' => $model->id,
26 | 'deleted_at' => $model->deleted_at
27 | ], $model::withTrashed()->first()->toArray());
28 | }
29 |
30 | /**
31 | * @test
32 | */
33 | public function itWillGetTheCorrectDeletedAtColumnOnTheMainTable()
34 | {
35 | /** @var Post $model */
36 | $model = factory(Post::class)->create([]);
37 |
38 | $this->assertEquals('posts.deleted_at', $model->getQualifiedDeletedAtColumn());
39 | }
40 |
41 | /**
42 | * @test
43 | */
44 | public function itWillGetTheCorrectDeletedAtColumnOnTheVersionTable()
45 | {
46 | /** @var User $model */
47 | $model = factory(User::class)->create([]);
48 |
49 | $this->assertEquals('users_version.deleted_at', $model->getQualifiedDeletedAtColumn());
50 | }
51 |
52 | /**
53 | * @test
54 | * @throws \Exception
55 | */
56 | public function itWillSaveDeletedAtInTheMainTable()
57 | {
58 | /** @var Post $model */
59 | $model = factory(Post::class)->create([]);
60 | $model->delete();
61 |
62 | $this->assertDatabaseHas('posts', [
63 | 'id' => $model->id,
64 | 'deleted_at' => $model->deleted_at
65 | ]);
66 | }
67 |
68 | /**
69 | * @test
70 | * @throws \Exception
71 | */
72 | public function itWillSaveDeletedAtInTheVersionTable()
73 | {
74 | /** @var User $model */
75 | $model = factory(User::class)->create([]);
76 | $model->delete();
77 |
78 | $this->assertDatabaseHas('users_version', [
79 | 'ref_id' => $model->id,
80 | 'version' => 1,
81 | 'deleted_at' => null
82 | ]);
83 |
84 | $this->assertDatabaseHas('users_version', [
85 | 'ref_id' => $model->id,
86 | 'version' => 2,
87 | 'deleted_at' => $model->deleted_at->format('Y-m-d H:i:s')
88 | ]);
89 | }
90 |
91 | public function modelProvider()
92 | {
93 | return [
94 | [
95 | User::class
96 | ],
97 | [
98 | Post::class
99 | ]
100 | ];
101 | }
102 | }
103 |
--------------------------------------------------------------------------------
/tests/Unit/VersionableTest.php:
--------------------------------------------------------------------------------
1 | create([]);
18 |
19 | $this->assertDatabaseHas($model->getTable(), [
20 | 'id' => $model->id,
21 | 'username' => $model->username,
22 | ]);
23 |
24 | $this->assertDatabaseHas($model->getVersionTable(), [
25 | 'ref_id' => $model->id,
26 | 'version' => $model->latest_version,
27 | 'email' => $model->email,
28 | 'city' => $model->city
29 | ]);
30 | }
31 |
32 | /**
33 | * @test
34 | */
35 | public function itWillVersionModelsWhenUpdating()
36 | {
37 | /** @var User $model */
38 | $model = factory(User::class)->create([]);
39 | $email = $model->email;
40 |
41 | $model->update([
42 | 'email' => 'rick@wubba-lubba-dub.dub'
43 | ]);
44 |
45 | $this->assertDatabaseHas($model->getTable(), [
46 | 'username' => $model->username,
47 | ]);
48 |
49 | $this->assertDatabaseHas($model->getVersionTable(), [
50 | 'ref_id' => $model->id,
51 | 'version' => 1,
52 | 'email' => $email,
53 | 'city' => $model->city
54 | ]);
55 |
56 | $this->assertDatabaseHas($model->getVersionTable(), [
57 | 'ref_id' => $model->id,
58 | 'version' => 2,
59 | 'email' => $model->email,
60 | 'city' => $model->city
61 | ]);
62 | }
63 |
64 | /**
65 | * @test
66 | */
67 | public function itWillVersionModelsWhenSaving()
68 | {
69 | /** @var User $model */
70 | $model = factory(User::class)->create([]);
71 | $email = $model->email;
72 |
73 | $model->email = 'rick@wubba-lubba-dub.dub';
74 | $model->save();
75 |
76 | $this->assertDatabaseHas($model->getTable(), [
77 | 'username' => $model->username,
78 | ]);
79 |
80 | $this->assertDatabaseHas($model->getVersionTable(), [
81 | 'ref_id' => $model->id,
82 | 'version' => 1,
83 | 'email' => $email,
84 | 'city' => $model->city
85 | ]);
86 |
87 | $this->assertDatabaseHas($model->getVersionTable(), [
88 | 'ref_id' => $model->id,
89 | 'version' => 2,
90 | 'email' => $model->email,
91 | 'city' => $model->city
92 | ]);
93 | }
94 |
95 | /**
96 | * @test
97 | */
98 | public function itWillVersionModelsWhenInserting()
99 | {
100 | /** @var User $model */
101 | $model = factory(User::class)->make([]);
102 | $model->created_at = Carbon::now();
103 | $model->updated_at = Carbon::now();
104 |
105 | User::insert($model->toArray());
106 |
107 | $this->assertDatabaseHas($model->getTable(), [
108 | 'id' => 1,
109 | 'username' => $model->username,
110 | ]);
111 |
112 | $this->assertDatabaseHas($model->getVersionTable(), [
113 | 'ref_id' => 1,
114 | 'version' => 1,
115 | 'email' => $model->email,
116 | 'city' => $model->city
117 | ]);
118 | }
119 |
120 | /**
121 | * @test
122 | */
123 | public function itWillUpdateTheLatestVersionWhenCreating()
124 | {
125 | /** @var User $model */
126 | $model = factory(User::class)->create([]);
127 |
128 | $this->assertEquals(1, $model->latest_version);
129 | }
130 |
131 | /**
132 | * @test
133 | */
134 | public function itWillUpdateTheLatestVersionWhenUpdating()
135 | {
136 | /** @var User $model */
137 | $model = factory(User::class)->create([]);
138 |
139 | $model->update([
140 | 'email' => 'rick@wubba-lubba-dub.dub'
141 | ]);
142 |
143 | $this->assertEquals(2, $model->latest_version);
144 | }
145 |
146 | /**
147 | * @test
148 | */
149 | public function itWillUpdateTheLatestVersionWhenSaving()
150 | {
151 | /** @var User $model */
152 | $model = factory(User::class)->create([]);
153 |
154 | $model->email = 'rick@wubba-lubba-dub.dub';
155 | $model->save();
156 |
157 | $this->assertEquals(2, $model->latest_version);
158 | }
159 |
160 | /**
161 | * @test
162 | */
163 | public function itWillOnlyVersionVersionedAttributes()
164 | {
165 | /** @var User $model */
166 | $model = factory(User::class)->create([]);
167 | $email = $model->email;
168 |
169 | $model->email = 'rick@wubba-lubba-dub.dub';
170 | $model->username = 'RickSanchez';
171 | $model->save();
172 |
173 | $this->assertDatabaseHas($model->getTable(), [
174 | 'username' => 'RickSanchez',
175 | ]);
176 |
177 | $this->assertDatabaseHas($model->getVersionTable(), [
178 | 'ref_id' => $model->id,
179 | 'version' => 1,
180 | 'email' => $email,
181 | 'city' => $model->city
182 | ]);
183 |
184 | $this->assertDatabaseHas($model->getVersionTable(), [
185 | 'ref_id' => $model->id,
186 | 'version' => 2,
187 | 'email' => $model->email,
188 | 'city' => $model->city
189 | ]);
190 | }
191 | }
192 |
--------------------------------------------------------------------------------
/tests/database/factories/PostFactory.php:
--------------------------------------------------------------------------------
1 | define(Post::class, function (Faker $faker) {
13 | return [
14 | 'title' => $faker->title,
15 | 'content' => $faker->text,
16 | ];
17 | });
--------------------------------------------------------------------------------
/tests/database/factories/UserFactory.php:
--------------------------------------------------------------------------------
1 | define(User::class, function (Faker $faker) {
13 | return [
14 | 'email' => $faker->unique()->safeEmail,
15 | 'username' => $faker->userName,
16 | 'city' => $faker->city
17 | ];
18 | });
--------------------------------------------------------------------------------
/tests/database/migrations/2014_10_12_000000_create_users_table.php:
--------------------------------------------------------------------------------
1 | increments('id');
18 | $table->integer('latest_version');
19 | $table->string('username');
20 | $table->timestamp('created_at');
21 | });
22 |
23 | Schema::create('users_version', function(Blueprint $table) {
24 | $table->integer('ref_id')->unsigned();
25 | $table->integer('version')->unsigned();
26 | $table->string('email');
27 | $table->string('city');
28 | $table->timestamp('updated_at');
29 | $table->softDeletes();
30 |
31 | $table->primary(['ref_id', 'version']);
32 | });
33 | }
34 |
35 | /**
36 | * Reverse the migrations.
37 | *
38 | * @return void
39 | */
40 | public function down()
41 | {
42 | Schema::dropIfExists('users');
43 | Schema::dropIfExists('users_version');
44 | }
45 | }
46 |
--------------------------------------------------------------------------------
/tests/database/migrations/2014_10_12_100000_create_posts_table.php:
--------------------------------------------------------------------------------
1 | increments('id');
18 | $table->integer('latest_version');
19 | $table->string('title');
20 | $table->timestamp('created_at');
21 | $table->softDeletes();
22 | });
23 |
24 | Schema::create('posts_version', function(Blueprint $table) {
25 | $table->integer('ref_id')->unsigned();
26 | $table->integer('version')->unsigned();
27 | $table->text('content');
28 | $table->timestamp('updated_at');
29 |
30 | $table->primary(['ref_id', 'version']);
31 | });
32 | }
33 |
34 | /**
35 | * Reverse the migrations.
36 | *
37 | * @return void
38 | */
39 | public function down()
40 | {
41 | Schema::dropIfExists('posts');
42 | Schema::dropIfExists('posts_version');
43 | }
44 | }
45 |
--------------------------------------------------------------------------------