Skip to content

Commit

Permalink
Add mapping on builder
Browse files Browse the repository at this point in the history
  • Loading branch information
jarektkaczyk committed Mar 28, 2015
1 parent e07bf8a commit 1092efd
Show file tree
Hide file tree
Showing 6 changed files with 251 additions and 6 deletions.
29 changes: 24 additions & 5 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -42,23 +42,27 @@ If you want to know more about new extensions you can check our [Roadmap](#roadm

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

Define mappings on the protected `$maps` variable like bellow.
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))

```php
<?php namespace App;

use Sofa\Eloquence\Mappable; // trait
use Sofa\Eloquence\Contracts\Mappable as MappableContract; // interface

class User extends \Eloquent {
class User extends \Eloquent implements MappableContract {

use Mappable;

protected $maps = [
// implicit mapping:
// implicit relation mapping:
'profile' => ['first_name', 'last_name'],

// explicit mapping:
'picture' => 'profile.piture_path'
// explicit relation mapping:
'picture' => 'profile.piture_path',

// simple alias
'dev_friendly_name' => 'badlynamedcolumn',
];

public function profile()
Expand Down Expand Up @@ -100,6 +104,21 @@ $user->profile->save();
$user->push();
```

**NEW** Now you can also query the mappings:

```php
// simple alias
User::where('dev_friendly_name', 'some_value')->toSql();
// select * from users where badlynamedcolumn = 'some_value'

// relation mapping
User::where('first_name', 'Romain Lanz')->toSql(); // uses whereHas
// select * from users where (
// select count(*) from profiles
// where users.profile_id = profiles.id and first_name = 'Romain Lanz'
// ) >= 1
```


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

Expand Down
13 changes: 12 additions & 1 deletion composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,15 @@
"name": "sofa/eloquence",
"description": "Extensions for the Eloquent ORM.",
"license": "MIT",
"keywords": [
"laravel",
"eloquent",
"orm",
"database",
"active record",
"activerecord",
"sql"
],
"authors": [
{
"name": "Jarek Tkaczyk",
Expand All @@ -16,7 +25,9 @@
},
"require-dev": {
"phpunit/phpunit": "~4.0",
"squizlabs/php_codesniffer": "~2.0"
"squizlabs/php_codesniffer": "~2.0",
"mockery/mockery": "~0.9",
"illuminate/database": "~5.0"
},
"autoload": {
"psr-4": {
Expand Down
107 changes: 107 additions & 0 deletions src/Builder.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,107 @@
<?php namespace Sofa\Eloquence;

use Illuminate\Database\Eloquent\Builder as EloquentBuilder;
use Sofa\Eloquence\Contracts\Mappable as MappableContract;

This comment has been minimized.

Copy link
@RomainLanz

RomainLanz Mar 28, 2015

Contributor

You didn't implement it.

This comment has been minimized.

Copy link
@jarektkaczyk

jarektkaczyk Mar 28, 2015

Author Owner

It's instanceof check only on line 55.

This comment has been minimized.

Copy link
@RomainLanz

RomainLanz Mar 28, 2015

Contributor

Didn't see it. Sorry for this useless note.


class Builder extends EloquentBuilder
{

/**
* Add where constraint to the query.
*
* @param string $column
* @param string $operator
* @param mixed $value
* @param string $boolean
* @return $this
*/
public function where($column, $operator = null, $value = null, $boolean = 'and')
{
// If developer provided column prefixed with table name we will
// not even try to map the column, since obviously the value
// refers to the actual column name on the queried table
if ($this->notPrefixed($column)) {
$column = $this->getColumnMapping($column);

if ($this->nestedMapping($column)) {
return $this->mappedWhere($column, $operator, $value, $boolean);
}
}

return parent::where($column, $operator, $value, $boolean);
}

/**
* Determine whether the column was not passed with table prefix.
*
* @param string $column
* @return boolean
*/
protected function notPrefixed($column)
{
return strpos($column, '.') === false;
}

/**
* Get the mapping for a column if exists or simply return the column.
*
* @param string $column
* @return string
*/
protected function getColumnMapping($column)
{
$model = $this->getModel();

if (is_string($column) && $model instanceof MappableContract && $model->hasMapping($column)) {
$column = $model->getMappingForAttribute($column);
}

return $column;
}

/**
* Determine whether the mapping points to relation.
*
* @param string $mapping
* @return boolean
*/
protected function nestedMapping($mapping)
{
return strpos($mapping, '.') !== false;
}

/**
* Add a relationship count condition to the query with where clauses.
*
* @param string $mapping
* @param string $operator
* @param mixed $value
* @param string $boolean
* @return $this
*/
protected function mappedWhere($mapping, $operator, $value, $boolean)
{
list($target, $column) = $this->parseMapping($mapping);

return $this->has($target, '>=', 1, $boolean, function ($q) use ($column, $operator, $value) {
$q->where($column, $operator, $value);
});
}

/**
* Get the target relation and column from the mapping.
*
* @param string $mapping
* @return array
*/
protected function parseMapping($mapping)
{
$segments = explode('.', $mapping);

$column = array_pop($segments);

$target = implode('.', $segments);

return [$target, $column];
}
}
10 changes: 10 additions & 0 deletions src/Contracts/Mappable.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
<?php namespace Sofa\Eloquence\Contracts;

interface Mappable
{
public function hasMapping($key);
public function mapAttribute($key);
public function getMappingForAttribute($key);
public function getMaps();
public function setMaps(array $mappings);
}
10 changes: 10 additions & 0 deletions src/Mappable.php
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,16 @@
*/
trait Mappable
{
/**
* @codeCoverageIgnore
*
* @param \Illuminate\Database\Query\Builder $query
* @return \Sofa\Eloquence\Builder
*/
public function newEloquentBuilder($query)
{
return new Builder($query);
}

/**
* @codeCoverageIgnore
Expand Down
88 changes: 88 additions & 0 deletions tests/BuilderTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
<?php namespace Sofa\Eloquence\Tests;

use Sofa\Eloquence\Builder;
use Sofa\Eloquence\Mappable;
use Sofa\Eloquence\Contracts\Mappable as MappableContract;

use Illuminate\Database\Query\Builder as Query;
use Illuminate\Database\Eloquent\Model;

use Mockery as m;

class BuilderTest extends \PHPUnit_Framework_TestCase {

public function setUp()
{
$this->model = new ModelStub;
}

/**
* @test
* @covers \Sofa\Eloquence\Builder::where
* @covers \Sofa\Eloquence\Builder::notPrefixed
* @covers \Sofa\Eloquence\Builder::getColumnMapping
* @covers \Sofa\Eloquence\Builder::nestedMapping
*/
public function it_adds_where_constraint_for_alias_mapping()
{
$builder = $this->getBuilder();

$sql = $builder->where('foo', 'value')->toSql();

$this->assertEquals('select * from "table" where "bar" = ?', $sql);
}

/**
* @test
* @covers \Sofa\Eloquence\Builder::where
* @covers \Sofa\Eloquence\Builder::mappedWhere
* @covers \Sofa\Eloquence\Builder::notPrefixed
* @covers \Sofa\Eloquence\Builder::getColumnMapping
* @covers \Sofa\Eloquence\Builder::nestedMapping
* @covers \Sofa\Eloquence\Builder::parseMapping
*/
public function it_adds_where_constraint_for_nested_mappings()
{
$alias = 'aliased_column';
$target = 'deeply.nested.relation';
$mapping = $target . '.column';

$connection = m::mock('\Illuminate\Database\ConnectionInterface');
$processor = m::mock('\Illuminate\Database\Query\Processors\Processor');
$grammar = m::mock('\Illuminate\Database\Query\Grammars\Grammar');
$query = new Query($connection, $grammar, $processor);

$model = m::mock('\Sofa\Eloquence\Contracts\Mappable');
$model->shouldReceive('hasMapping')->with($alias)->andReturn(true);
$model->shouldReceive('getMappingForAttribute')->with($alias)->andReturn($mapping);

$builder = m::mock('\Sofa\Eloquence\Builder[has,getModel]', [$query]);
$builder->shouldReceive('getModel')->andReturn($model);
$builder->shouldReceive('has')->with($target, '>=', 1, 'and', m::type('callable'))->andReturn($builder);

$builder->where('aliased_column', 'value');
}

protected function getBuilder()
{
$grammar = new \Illuminate\Database\Query\Grammars\Grammar;
$connection = m::mock('\Illuminate\Database\ConnectionInterface');
$processor = m::mock('\Illuminate\Database\Query\Processors\Processor');
$query = new Query($connection, $grammar, $processor);
$builder = new Builder($query);

$model = new MappableStub;
$builder->setModel($model);

return $builder;
}
}

class MappableStub extends Model implements MappableContract {
use Mappable;

protected $table = 'table';
protected $maps = [
'foo' => 'bar',
];
}

0 comments on commit 1092efd

Please sign in to comment.