Skip to content

Commit 1092efd

Browse files
committed
Add mapping on builder
1 parent e07bf8a commit 1092efd

File tree

6 files changed

+251
-6
lines changed

6 files changed

+251
-6
lines changed

README.md

+24-5
Original file line numberDiff line numberDiff line change
@@ -42,23 +42,27 @@ If you want to know more about new extensions you can check our [Roadmap](#roadm
4242

4343
# <a name="mappable"></a>Mappable
4444

45-
Define mappings on the protected `$maps` variable like bellow.
45+
Define mappings on the protected `$maps` variable like bellow. Use this extension in order to map your 1-1 relations AND/OR simple column aliasing (eg. if you work with legacy DB with fields like `FIELD_01` or `somereallyBad_and_long_name` - inspired by [@treythomas123](https://github.com/laravel/framework/pull/8200))
4646

4747
```php
4848
<?php namespace App;
4949

5050
use Sofa\Eloquence\Mappable; // trait
51+
use Sofa\Eloquence\Contracts\Mappable as MappableContract; // interface
5152

52-
class User extends \Eloquent {
53+
class User extends \Eloquent implements MappableContract {
5354

5455
use Mappable;
5556

5657
protected $maps = [
57-
// implicit mapping:
58+
// implicit relation mapping:
5859
'profile' => ['first_name', 'last_name'],
5960

60-
// explicit mapping:
61-
'picture' => 'profile.piture_path'
61+
// explicit relation mapping:
62+
'picture' => 'profile.piture_path',
63+
64+
// simple alias
65+
'dev_friendly_name' => 'badlynamedcolumn',
6266
];
6367

6468
public function profile()
@@ -100,6 +104,21 @@ $user->profile->save();
100104
$user->push();
101105
```
102106

107+
**NEW** Now you can also query the mappings:
108+
109+
```php
110+
// simple alias
111+
User::where('dev_friendly_name', 'some_value')->toSql();
112+
// select * from users where badlynamedcolumn = 'some_value'
113+
114+
// relation mapping
115+
User::where('first_name', 'Romain Lanz')->toSql(); // uses whereHas
116+
// select * from users where (
117+
// select count(*) from profiles
118+
// where users.profile_id = profiles.id and first_name = 'Romain Lanz'
119+
// ) >= 1
120+
```
121+
103122

104123
## <a name="explicit-vs-implicit-mappings"></a>Explicit vs. Implicit mappings
105124

composer.json

+12-1
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,15 @@
22
"name": "sofa/eloquence",
33
"description": "Extensions for the Eloquent ORM.",
44
"license": "MIT",
5+
"keywords": [
6+
"laravel",
7+
"eloquent",
8+
"orm",
9+
"database",
10+
"active record",
11+
"activerecord",
12+
"sql"
13+
],
514
"authors": [
615
{
716
"name": "Jarek Tkaczyk",
@@ -16,7 +25,9 @@
1625
},
1726
"require-dev": {
1827
"phpunit/phpunit": "~4.0",
19-
"squizlabs/php_codesniffer": "~2.0"
28+
"squizlabs/php_codesniffer": "~2.0",
29+
"mockery/mockery": "~0.9",
30+
"illuminate/database": "~5.0"
2031
},
2132
"autoload": {
2233
"psr-4": {

src/Builder.php

+107
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,107 @@
1+
<?php namespace Sofa\Eloquence;
2+
3+
use Illuminate\Database\Eloquent\Builder as EloquentBuilder;
4+
use Sofa\Eloquence\Contracts\Mappable as MappableContract;
5+
6+
class Builder extends EloquentBuilder
7+
{
8+
9+
/**
10+
* Add where constraint to the query.
11+
*
12+
* @param string $column
13+
* @param string $operator
14+
* @param mixed $value
15+
* @param string $boolean
16+
* @return $this
17+
*/
18+
public function where($column, $operator = null, $value = null, $boolean = 'and')
19+
{
20+
// If developer provided column prefixed with table name we will
21+
// not even try to map the column, since obviously the value
22+
// refers to the actual column name on the queried table
23+
if ($this->notPrefixed($column)) {
24+
$column = $this->getColumnMapping($column);
25+
26+
if ($this->nestedMapping($column)) {
27+
return $this->mappedWhere($column, $operator, $value, $boolean);
28+
}
29+
}
30+
31+
return parent::where($column, $operator, $value, $boolean);
32+
}
33+
34+
/**
35+
* Determine whether the column was not passed with table prefix.
36+
*
37+
* @param string $column
38+
* @return boolean
39+
*/
40+
protected function notPrefixed($column)
41+
{
42+
return strpos($column, '.') === false;
43+
}
44+
45+
/**
46+
* Get the mapping for a column if exists or simply return the column.
47+
*
48+
* @param string $column
49+
* @return string
50+
*/
51+
protected function getColumnMapping($column)
52+
{
53+
$model = $this->getModel();
54+
55+
if (is_string($column) && $model instanceof MappableContract && $model->hasMapping($column)) {
56+
$column = $model->getMappingForAttribute($column);
57+
}
58+
59+
return $column;
60+
}
61+
62+
/**
63+
* Determine whether the mapping points to relation.
64+
*
65+
* @param string $mapping
66+
* @return boolean
67+
*/
68+
protected function nestedMapping($mapping)
69+
{
70+
return strpos($mapping, '.') !== false;
71+
}
72+
73+
/**
74+
* Add a relationship count condition to the query with where clauses.
75+
*
76+
* @param string $mapping
77+
* @param string $operator
78+
* @param mixed $value
79+
* @param string $boolean
80+
* @return $this
81+
*/
82+
protected function mappedWhere($mapping, $operator, $value, $boolean)
83+
{
84+
list($target, $column) = $this->parseMapping($mapping);
85+
86+
return $this->has($target, '>=', 1, $boolean, function ($q) use ($column, $operator, $value) {
87+
$q->where($column, $operator, $value);
88+
});
89+
}
90+
91+
/**
92+
* Get the target relation and column from the mapping.
93+
*
94+
* @param string $mapping
95+
* @return array
96+
*/
97+
protected function parseMapping($mapping)
98+
{
99+
$segments = explode('.', $mapping);
100+
101+
$column = array_pop($segments);
102+
103+
$target = implode('.', $segments);
104+
105+
return [$target, $column];
106+
}
107+
}

src/Contracts/Mappable.php

+10
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
<?php namespace Sofa\Eloquence\Contracts;
2+
3+
interface Mappable
4+
{
5+
public function hasMapping($key);
6+
public function mapAttribute($key);
7+
public function getMappingForAttribute($key);
8+
public function getMaps();
9+
public function setMaps(array $mappings);
10+
}

src/Mappable.php

+10
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,16 @@
77
*/
88
trait Mappable
99
{
10+
/**
11+
* @codeCoverageIgnore
12+
*
13+
* @param \Illuminate\Database\Query\Builder $query
14+
* @return \Sofa\Eloquence\Builder
15+
*/
16+
public function newEloquentBuilder($query)
17+
{
18+
return new Builder($query);
19+
}
1020

1121
/**
1222
* @codeCoverageIgnore

tests/BuilderTest.php

+88
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,88 @@
1+
<?php namespace Sofa\Eloquence\Tests;
2+
3+
use Sofa\Eloquence\Builder;
4+
use Sofa\Eloquence\Mappable;
5+
use Sofa\Eloquence\Contracts\Mappable as MappableContract;
6+
7+
use Illuminate\Database\Query\Builder as Query;
8+
use Illuminate\Database\Eloquent\Model;
9+
10+
use Mockery as m;
11+
12+
class BuilderTest extends \PHPUnit_Framework_TestCase {
13+
14+
public function setUp()
15+
{
16+
$this->model = new ModelStub;
17+
}
18+
19+
/**
20+
* @test
21+
* @covers \Sofa\Eloquence\Builder::where
22+
* @covers \Sofa\Eloquence\Builder::notPrefixed
23+
* @covers \Sofa\Eloquence\Builder::getColumnMapping
24+
* @covers \Sofa\Eloquence\Builder::nestedMapping
25+
*/
26+
public function it_adds_where_constraint_for_alias_mapping()
27+
{
28+
$builder = $this->getBuilder();
29+
30+
$sql = $builder->where('foo', 'value')->toSql();
31+
32+
$this->assertEquals('select * from "table" where "bar" = ?', $sql);
33+
}
34+
35+
/**
36+
* @test
37+
* @covers \Sofa\Eloquence\Builder::where
38+
* @covers \Sofa\Eloquence\Builder::mappedWhere
39+
* @covers \Sofa\Eloquence\Builder::notPrefixed
40+
* @covers \Sofa\Eloquence\Builder::getColumnMapping
41+
* @covers \Sofa\Eloquence\Builder::nestedMapping
42+
* @covers \Sofa\Eloquence\Builder::parseMapping
43+
*/
44+
public function it_adds_where_constraint_for_nested_mappings()
45+
{
46+
$alias = 'aliased_column';
47+
$target = 'deeply.nested.relation';
48+
$mapping = $target . '.column';
49+
50+
$connection = m::mock('\Illuminate\Database\ConnectionInterface');
51+
$processor = m::mock('\Illuminate\Database\Query\Processors\Processor');
52+
$grammar = m::mock('\Illuminate\Database\Query\Grammars\Grammar');
53+
$query = new Query($connection, $grammar, $processor);
54+
55+
$model = m::mock('\Sofa\Eloquence\Contracts\Mappable');
56+
$model->shouldReceive('hasMapping')->with($alias)->andReturn(true);
57+
$model->shouldReceive('getMappingForAttribute')->with($alias)->andReturn($mapping);
58+
59+
$builder = m::mock('\Sofa\Eloquence\Builder[has,getModel]', [$query]);
60+
$builder->shouldReceive('getModel')->andReturn($model);
61+
$builder->shouldReceive('has')->with($target, '>=', 1, 'and', m::type('callable'))->andReturn($builder);
62+
63+
$builder->where('aliased_column', 'value');
64+
}
65+
66+
protected function getBuilder()
67+
{
68+
$grammar = new \Illuminate\Database\Query\Grammars\Grammar;
69+
$connection = m::mock('\Illuminate\Database\ConnectionInterface');
70+
$processor = m::mock('\Illuminate\Database\Query\Processors\Processor');
71+
$query = new Query($connection, $grammar, $processor);
72+
$builder = new Builder($query);
73+
74+
$model = new MappableStub;
75+
$builder->setModel($model);
76+
77+
return $builder;
78+
}
79+
}
80+
81+
class MappableStub extends Model implements MappableContract {
82+
use Mappable;
83+
84+
protected $table = 'table';
85+
protected $maps = [
86+
'foo' => 'bar',
87+
];
88+
}

0 commit comments

Comments
 (0)