Skip to content

Instantly share code, notes, and snippets.

@beeblebrox3
Created July 29, 2014 18:53
Show Gist options
  • Star 2 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save beeblebrox3/2cfc736027b37537f69a to your computer and use it in GitHub Desktop.
Save beeblebrox3/2cfc736027b37537f69a to your computer and use it in GitHub Desktop.
<?php
namespace Sigep\EloquentEnhancements\Traits;
use Illuminate\Database\Eloquent\Relations\BelongsToMany;
use Illuminate\Database\Eloquent\Relations\HasMany;
use Illuminate\Database\Eloquent\Relations\MorphMany;
use Illuminate\Database\Eloquent\Relations\HasManyThrough;
use Illuminate\Mail\Message;
use Illuminate\Support\MessageBag;
use Psr\Log\InvalidArgumentException;
use Sigep\Support\ArrayHelper;
trait SaveAll
{
/**
* Put the id of current object as foreign key in all arrays inside $data
* Util when a relationship of a relationship depends of the id from current model
* as foreign key to.
* To avoid problems, data must to have the foreign key with "auto" value. Just in case
* that the records belongs, for any reason, to another object
* @param array $data
* @return array
*/
private function fillForeignKeyRecursively(array $data)
{
$foreign = $this->getForeignKey();
foreach ($data as &$piece) {
if (is_array($piece)) {
$piece = $this->fillForeignKeyRecursively($piece);
}
}
if (isset($data[$foreign]) && $data[$foreign] == 'auto' && $this->id) {
$data[$foreign] = $this->id;
}
return $data;
}
/**
* create a new object and calls saveAll() method to save its relationships
* @param array $data
* @param string $path used to control where put the error messages
* @return boolean
*/
public function createAll(array $data = [], $path = '')
{
$this->fill($data);
if (!$this->save()) {
return false;
}
$data = $this->fillForeignKeyRecursively($data);
return $this->saveAll($data, true, $path);
}
/**
* Update current record and create/update its related data
* The related data must be array and the key is the name of the relationship
* We support relationships from relationships too.
*
* @param array $data
* @param boolean $skipUpdate if true, current model will not be changed, just the relationships
* @return boolean
*/
public function saveAll(array $data = [], $skipUpdate = false, $path = '')
{
$this->fill($data);
if (!$skipUpdate && !$this->save()) {
return false;
}
$relationships = $this->getRelationshipsFromData($data);
// save relationships
foreach ($relationships as $relationship => $values) {
$currentPath = $path ? "{$path}." : '';
$currentPath .= $relationship;
// dont add more relationships that is allowed
$values = $this->applyRelationshipLimit($relationship, $values);
if (!$this->addRelated($relationship, $values, $currentPath)) {
return false;
}
}
return true;
}
/**
* Get the specified limit for $relationship or false if not exists
* @param $relationship name of the relationship
* @return mixed
*/
protected function getRelationshipLimit($relationship)
{
if (isset($this->relationshipsLimits[$relationship])) {
return $this->relationshipsLimits[$relationship];
}
return false;
}
/**
* Remove values from $values when the relationship limit is reached
* @param string $relationship relationship name
* @param array $values
* @return array modified $values
*/
protected function applyRelationshipLimit($relationship, $values)
{
$relationshipLimit = $this->getRelationshipLimit($relationship);
if (!$relationshipLimit) {
return $values;
}
$currentRelationships = count($this->$relationship);
$newRelationships = 0;
$removeRelationships = [];
if (ArrayHelper::isAssociative($values)) {
return $values;
}
foreach ($values as $key => $value) {
if (ArrayHelper::isEmpty($value)) {
unset($values[$key]);
continue;
}
if (!isset($value['id'])) {
$newRelationships++;
$removeRelationships[] = $key;
}
}
$diff = ($currentRelationships + $newRelationships) - $relationshipLimit;
if ($diff) {
$removeRelationships = array_slice($removeRelationships, -$diff);
foreach ($removeRelationships as $key) {
unset($values[$key]);
}
}
return array_values($values);
}
/**
* Add related data to the current model recursively
* @param string $relationshipName
* @param array $values
* @return bool
*/
public function addRelated($relationshipName, array $values, $path = '')
{
// get info from relationship
if (!method_exists($this, $relationshipName)) {
return true;
}
$relationship = $this->$relationshipName();
// if is a numeric array, recursive calls to add multiple related
if (!ArrayHelper::isAssociative($values)) {
$position = 0;
foreach ($values as $value) {
if (!$this->addRelated($relationshipName, $value, $path . '.' . $position++)) {
return false;
}
}
return true;
}
// if has not data, skipp
if (ArrayHelper::isEmpty($values)) {
return true;
}
// set foreign for hasMany relationships
if ($relationship instanceof HasMany) {
$values[last(explode('.', $relationship->getForeignKey()))] = $this->id;
}
// if is MorphToMany, put other foreign and fill the type
if ($relationship instanceof MorphMany) {
$values[$relationship->getPlainForeignKey()] = $this->id;
$values[$relationship->getPlainMorphType()] = get_class($this);
}
// if BelongsToMany, put current id in place
if ($relationship instanceof BelongsToMany) {
$values[last(explode('.', $relationship->getForeignKey()))] = $this->id;
}
// get targetModel
if ($relationship instanceof HasManyThrough) {
$model = $relationship->getParent();
} else {
$model = $relationship->getRelated();
}
// if has ID, delete or update
if (!empty($values['id'])) {
$obj = $model->find($values['id']);
if (!$obj) {
return false;
}
// delete or update?
if (!empty($values['_delete'])) {
$resultAction = $obj->delete();
} else {
// @todo put errors
$resultAction = $obj->saveAll($values);
}
return $resultAction;
}
// only BelongsToMany :)
if (!empty($values['_delete'])) {
$this->$relationshipName()->detach($values[last(explode('.', $relationship->getOtherKey()))]);
return true;
}
if (!empty($values['_create']) && $relationship instanceof BelongsToMany) {
$obj = $relationship->getRelated();
// if has conditions, fill the values)
// this helps to add fixed values in relationships using its conditions
// @todo experimental
foreach ($relationship->getQuery()->getQuery()->wheres as $where) {
$column = last(explode('.', $where['column']));
if (!empty($where['value']) && empty($values[$column])) {
$values[$column] = $where['value'];
}
}
if (!$obj->createAll($values)) {
$objErrors = $obj->errors()->toArray();
$thisErrors = $this->errors();
foreach ($objErrors as $field => $errors) {
foreach ($errors as $error ) {
$thisErrors->add(
"{$path}.{$field}",
$error
);
}
}
$this->setErrors($thisErrors);
return false;
}
$values[last(explode('.', $relationship->getOtherKey()))] = $obj->id;
}
if ($relationship instanceof HasMany || $relationship instanceof MorphMany) {
$relationshipObject = $relationship->getRelated();
} elseif ($relationship instanceof BelongsToMany) {
// if has a relationshipModel, use the model. Else, use attach
// attach doesn't return nothing
if (empty($this->relationshipsModels[$relationshipName])) {
$field = last(explode('.', $relationship->getOtherKey()));
$this->$relationshipName()->attach($values[$field]);
return true;
}
$relationshipObject = $this->relationshipsModels[$relationshipName];
$relationshipObject = new $relationshipObject;
} elseif ($relationship instanceof HasManyThrough) {
$relationshipObject = $model;
}
if (!$relationshipObject->createAll($values)) {
$objErrors = $relationshipObject->errors()->toArray();
$thisErrors = $this->errors();
if (! $thisErrors instanceof MessageBag) {
$thisErrors = new MessageBag();
}
foreach ($objErrors as $field => $errors) {
foreach ($errors as $error ) {
$thisErrors->add(
"{$path}.{$field}",
$error
);
}
}
$this->setErrors($thisErrors);
return false;
}
return true;
}
/**
* get values that are array in $data
* use this function to extract relationships from Input::all(), for example
* @param array $data
* @return array
*/
public function getRelationshipsFromData(array $data = [])
{
$relationships = [];
foreach ($data as $key => $value) {
if (is_array($value) && !is_numeric($key)) {
$relationships[$key] = $value;
}
}
return $relationships;
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment