/** * Perform a deserialization * * @param array|Closure $serializationProvider An anonymized serialization by array or generator * @return array An array consisting of the encountered internal ids as keys, and created db ids as values * @throws Exception on invalid rules */ public function deserialize($serializationProvider) { if (is_array($serializationProvider)) { // Pretend the array is an generator $generator = function () use($serializationProvider) { return [$serializationProvider]; }; } elseif ($serializationProvider instanceof Closure) { // Assume the given closure is a generator $generator = $serializationProvider; } else { throw new Exception("Provided serialization provider must be either an array or a generator"); } // Crank out the serialized data foreach ($generator() as $serialization) { // Count entities for simple progress functionality $entityCount = 0; foreach ($serialization as $items) { $entityCount += count($items); } // Set the goal to include all entities $this->state->setProgressGoal($entityCount); // Iterate through the serialization data Array<FQCN, Array<Serialized entity>> foreach ($serialization as $fqcn => $serializedEntities) { $this->state->push($fqcn); // Debug // Iterate through the serialized entities foreach ($serializedEntities as $serializedEntity) { $model = $this->app->make($fqcn); // Get the internal id of this entity $id = $serializedEntity["@id"]; // Has primary key in data? if (isset($serializedEntity[$model->getKeyName()])) { // Get database id $dbId = $serializedEntity[$model->getKeyName()]; // Set manually $model->id = $dbId; $model->exists = true; } // Get blueprints for this model $blueprint = $this->getBlueprint($model, $serializedEntity); // If blueprint is false, we don't want to deserialize at all if ($blueprint === false) { continue; } foreach ($blueprint as $rule) { if (is_array($rule)) { $field = $rule[0]; } else { $field = $rule; } $this->state->push($field); if (!method_exists($model, $field)) { // Non-relation // If the rule is an array, it's supposed contain a set of rules if (is_array($rule) && is_array($serializedEntity[$field])) { // Match rules for this field found, figure out how to use them if (count($rule) === 2 && is_array($serializedEntity[$field])) { // Rules // $rule[0] = field name // $rule[1] = array of rules // serialized content = array $model->{$field} = $this->toDbIds($model, $id, $field, $serializedEntity[$field], $rule[1]); } elseif (count($rule) === 3) { // Conditional Rules // $rule[0] = field name // $rule[1] = field name to match condition against // $rule[2] = associative array where key is the value to match the field against, // and the value is the array of rules to use when a match is found $matchAgainst = $serializedEntity[$rule[1]]; if (isset($rule[2][$matchAgainst])) { $model->{$field} = $this->toDbIds($model, $id, $field, $serializedEntity[$field], $rule[2][$matchAgainst]); } else { $model->{$field} = $serializedEntity[$field]; } } else { throw new Exception("Unsupported rule with weird array count of " . count($rule)); } } else { // The rule or content weren't arrays, so we just want to save the content $model->{$field} = $serializedEntity[$field]; } } else { // The field is a relation $relation = $model->{$field}(); $relationName = last(explode("\\", get_class($relation))); switch ($relationName) { case "BelongsTo": // Don't associate nulls if (isset($serializedEntity[$field])) { // Associate or defer an association $this->bookKeeper->associate($model, $field, $id, $serializedEntity[$field]); } break; case "MorphTo": // Morph or defer a morph $this->bookKeeper->morph($model, $field, $id, $serializedEntity[$field]); break; } } $this->state->pop(); // Debug } // Handle progress $this->state->incrementProgress(); // Create/Update the database entity $model->save(); // Report the real db id $this->bookKeeper->bind($id, $model->getKey()); } $this->state->pop(); // Debug } } // Resolve all deferred actions and return the book keeping Array<Internal id, Database id> return $this->bookKeeper->resolve(); }
/** * Perform a serialization * * @param Model $model Initially the top level model to which all of your other models are related * @param array $serialization Serialization tree being built * * @return array * @throws Exception */ public function serialize(Model $model, array $serialization = []) { $fqcn = get_class($model); $serializedEntity = []; // Get blueprints for this model $blueprint = $this->getBlueprint($model); // If blueprint is false, we don't want to serialize at all if ($blueprint === false) { return $serialization; } $this->state->push("{$fqcn}-" . $model->getKey()); // Debug // Set internal id (Directly from book keeper, don't use getIdRef) $serializedEntity["@id"] = $this->bookKeeper->getId($fqcn, $model->getKey()); // If blueprint is an associative array (as opposed to a normal array) we just want to merge with @id and continue $blueprintKeys = array_keys($blueprint); if (count($blueprintKeys) > 0 && is_string(reset($blueprintKeys))) { // First key is a string - Assume the whole thing is an associative array $serializedEntity = array_merge($serializedEntity, $blueprint); // [@id => @123] -> [@id => @123, foo => bar, ...] // Add to the "big" serialized array and return early $serialization[$fqcn][] = $serializedEntity; return $serialization; } // Make space in the serialized array for entities of this model type if (!isset($serialization[$fqcn])) { $serialization[$fqcn] = []; } // Iterate through the blueprint rules foreach ($blueprint as $rule) { if (is_array($rule)) { $field = $rule[0]; } else { $field = $rule; } $this->state->push($field); // Debug // Get content for the given field from the model $content = $model->{$field}; // Is some transformation involved? $transformer = null; if (method_exists($model, $field)) { // Yes, look at the transforming method by running it $transformer = $model->{$field}(); } // Do different things depending on the content type if (is_scalar($content) || is_null($content) || is_array($content)) { if (is_array($rule) && is_array($content)) { // Rules for this field found, figure out how to use them // [field, [match rules]] or [field, field to match against, [match1 => [match rules], match2 => ...]] if (count($rule) === 2) { // Match rules // [field, [match rules]] // $rule[0] = field name // $rule[1] = array of match rules // $content = array $serializedEntity[$field] = $this->toInternalIds($content, $rule[1]); } else { if (count($rule) === 3) { // Conditional rules // [field, field to match against, [match1 => [match rules], match2 => ...]] // $rule[0] = field name // $rule[1] = field name to match condition against // $rule[2] = associative array where key is the value to match the field against, // and the value is the array of rules to use when a match is found $matchAgainst = $model->{$rule[1]}; if (isset($rule[2][$matchAgainst])) { $serializedEntity[$field] = $this->toInternalIds($content, $rule[2][$matchAgainst]); } else { $serializedEntity[$field] = $content; } } else { throw new Exception("Unsupported rule with weird array count of " . count($rule)); } } } else { // The rule or content weren't arrays, so we just want to save the content $serializedEntity[$field] = $content; } } elseif ($content instanceof Model) { // The content is a model, implying a one-to-one relationship $contentFqcn = get_class($content); $relation = $transformer; $relationName = last(explode("\\", get_class($relation))); // Handle different relationships differently switch ($relationName) { case "BelongsTo": // Do we already have this belonging entity's internal id? if ($this->bookKeeper->hasId($contentFqcn, $content->{$relation->getOtherKey()})) { // Yep, use it and continue $serializedEntity[$field] = $this->getIdRef($contentFqcn, $content->{$relation->getOtherKey()}); } else { // Nope, create it and recurse down the rabbit hole $serializedEntity[$field] = $this->getIdRef($contentFqcn, $content->{$relation->getOtherKey()}); $serialization = $this->serialize($content, $serialization); } break; case "HasOne": // Recurse down the rabbit hole $serialization = $this->serialize($content, $serialization); break; case "MorphTo": // Do we already have this morphed entity's internal id? if ($this->bookKeeper->hasId($contentFqcn, $content->{$relation->getOtherKey()})) { // Yep, use it and continue $serializedEntity[$field] = $this->getIdRef($contentFqcn, $content->{$relation->getOtherKey()}); } else { // Nope, create it and recurse down the rabbit hole $serializedEntity[$field] = $this->getIdRef($contentFqcn, $content->{$relation->getOtherKey()}); $serialization = $this->serialize($content, $serialization); } // Get morphable id and morphable type and save as a tuple [type, id] $morphedId = $model->{$relation->getForeignKey()}; $morphedType = $model->{$relation->getMorphType()}; $serializedEntity[$field] = [$morphedType, $this->getIdRef($morphedType, $morphedId)]; break; } } elseif ($content instanceof Collection) { // The content is a Collection, implying a one-to-many relationship $relation = $model->{$field}(); $relationName = last(explode("\\", get_class($relation))); switch ($relationName) { case "HasMany": case "MorphMany": case "MorphToMany": // Iterate through the related entities foreach ($content as $child) { $serialization = $this->serialize($child, $serialization); } break; } } $this->state->pop(); // Debug } // Abort if entity is already processed // @todo Inefficient to do these checks at this point and not earlier foreach ($serialization[$fqcn] as $entity) { if ($entity["@id"] === $serializedEntity["@id"]) { $this->state->pop(); // Debug return $serialization; } } // Give the event listeners a chance to have a say if (isset($this->onBeforeAddToTree[$fqcn])) { // Run the callable $mutatedTree = $this->onBeforeAddToTree[$fqcn]($serializedEntity); // Did it return an array? if (is_array($mutatedTree)) { // Overwrite our original serialized entity data $serializedEntity = $mutatedTree; } else { if ($mutatedTree === false) { // Did it return a false? // Don't add the serialized entity at all - early return $this->state->pop(); // Debug // Return the serialization return $serialization; } } } // Add entity to the serialization $serialization[$fqcn][] = $serializedEntity; $this->state->pop(); // Debug // Return the serialization return $serialization; }