src/Sampler.php
<?php
namespace Bravesheep\Dogmatist;
use Bravesheep\Dogmatist\Exception\SampleException;
use Symfony\Component\PropertyAccess\Exception\NoSuchPropertyException;
use Symfony\Component\PropertyAccess\PropertyAccessorInterface;
class Sampler
{
/**
* @var Dogmatist
*/
private $dogmatist;
/**
* @var PropertyAccessorInterface
*/
private $accessor;
/**
* @var int
*/
private $unique_tries;
/**
* @var array
*/
private $sampled;
public function __construct(Dogmatist $dogmatist, PropertyAccessorInterface $accessor, $unique_tries = 128)
{
$this->dogmatist = $dogmatist;
$this->accessor = $accessor;
$this->unique_tries = $unique_tries;
$this->sampled = [];
}
/**
* @param Builder $builder
* @param int $count
* @return array
*/
public function samples(Builder $builder, $count)
{
$samples = [];
for ($i = 0; $i < $count; $i++) {
$samples[] = $this->sample($builder);
}
return $samples;
}
/**
* @param Builder $builder
* @return object|array
*/
public function sample(Builder $builder, UniqueArrayObject $parent = null)
{
$data = new UniqueArrayObject($parent);
$faker = $this->dogmatist->getFaker();
foreach ($builder->getFields() as $field) {
if (!$field->isType(Field::TYPE_NONE)) {
$generate = $field->isSingular() ? 1 : $faker->numberBetween($field->getMin(), $field->getMax());
$samples = [];
$mark = false;
for ($i = 0; $i < $generate; $i++) {
if ($field->isUnique()) {
$val = $this->sampleUniqueField($field, $data, $builder);
} else {
$val = $this->sampleField($field, $data);
}
if ($val instanceof ReplacableLink) {
$mark = true;
}
$samples[] = $val;
}
if ($field->isSingular()) {
$samples = $samples[0];
} elseif ($mark) {
$samples = new ReplacableArray($samples);
}
$data[$field->getName()] = $samples;
}
}
if ($builder instanceof ConstructorBuilder) {
return $data->getArrayCopy();
} else {
$result = $this->insertInObject($data->getArrayCopy(), $builder);
foreach ($builder->getListeners() as $listener) {
call_user_func($listener, $result);
}
return $result;
}
}
/**
* @param Field $field
* @param UniqueArrayObject $data
* @param Builder $builder
* @return mixed
* @throws SampleException
*/
private function sampleUniqueField(Field $field, UniqueArrayObject $data, Builder $builder)
{
// create a generation store for unique values
if ($field->isType(Field::TYPE_LINK)) {
// For links we only want uniqueness within the current object
// For unique relations with other objects across all samples a relation should be used.
$id = spl_object_hash($field) . $data->getId();
} else {
$id = spl_object_hash($field);
}
if (!isset($this->sampled[$id])) {
$this->sampled[$id] = [];
}
// try to iteratively generate a unique value
$rounds = 0;
do {
if ($rounds === $this->unique_tries) {
$name = $field->getName();
$type = $builder->getType();
throw new SampleException(
"Tried to get unique value for field {$name} in {$type}, but none could be generated"
);
}
$value = $this->sampleField($field, $data);
$rounds += 1;
} while (in_array($value, $this->sampled[$id], true));
// store the generated value for later testing
$this->sampled[$id][] = $value;
return $value;
}
/**
* @param Field $field
* @param UniqueArrayObject $data
* @return mixed
* @throws SampleException
*/
private function sampleField(Field $field, UniqueArrayObject $data)
{
$faker = $this->dogmatist->getFaker();
$type = $field->getType();
$value = null;
if (Field::TYPE_FAKE === $type) {
try {
$value = $faker->format($field->getFakedType(), $field->getFakedOptions());
} catch (\Exception $e) {
throw new SampleException("Could not fake value of type {$field->getFakedType()}", 0, $e);
}
} elseif (Field::TYPE_VALUE === $type || Field::TYPE_SELECT === $type) {
$value = $faker->randomElement($field->getSelection());
} elseif (Field::TYPE_RELATION === $type) {
$related = $field->getRelated();
$value = $this->sample($related, $data);
if ($related->hasParentLinks()) {
$value = new ReplacableLink($value, $related->getParentLinks());
}
} elseif (Field::TYPE_LINK === $type) {
$target = $field->getLinkTarget();
if (is_array($target)) {
$target = $faker->randomElement($target);
}
if ($this->dogmatist->getLinkManager()->hasUnlimitedSamples($target)) {
$value = $this->dogmatist->sample($target);
} else {
$samples = $this->dogmatist->getLinkManager()->samples($target);
$value = $faker->randomElement($samples);
}
} elseif (Field::TYPE_CALLBACK === $type) {
$callback = $field->getCallback();
$value = $callback($data, $this->dogmatist);
} else {
throw new SampleException("Could not generate data for field of type {$field->getType()}");
}
return $value;
}
/**
* @param array $data
* @param Builder $builder
* @return array|object
* @throws SampleException
*/
private function insertInObject(array $data, Builder $builder)
{
// special case for arrays: just return the array data
if ($builder->getType() === 'array' || $builder->getType() === '__construct') {
return $this->replaceLinksInArray($data, $builder);
}
// special case for generic objects (objects of type stdClass): just cast as an object
if ($builder->getType() === 'object' || $builder->getType() === 'stdClass') {
return $this->replaceLinksInObject((object) $data, $builder);
}
$refl = new \ReflectionClass($builder->getClass());
$obj = $this->constructObject($refl, $builder);
foreach ($data as $key => $val) {
$this->setObjectProperty($obj, $key, $val, $builder);
}
return $obj;
}
/**
* @param array $data
* @param Builder $builder
* @return array
* @throws SampleException
*/
private function replaceLinksInArray(array $data, Builder $builder)
{
foreach ($data as $key => &$value) {
if ($value instanceof ReplacableArray) {
foreach ($value->data as &$subval) {
foreach ($subval->fields as $field) {
$subval->value = $this->setObjectProperty($subval->value, $field, $data, $builder);
}
$subval = $subval->value;
}
$value = $value->data;
}
if ($value instanceof ReplacableLink) {
foreach ($value->fields as $field) {
$value->value = $this->setObjectProperty($value->value, $field, $data, $builder);
}
$value = $value->value;
}
}
return $data;
}
/**
* @param object $obj
* @param Builder $builder
* @return object
* @throws SampleException
*/
private function replaceLinksInObject($obj, Builder $builder)
{
foreach (get_object_vars($obj) as $key => $value) {
if ($value instanceof ReplacableArray) {
foreach ($value->data as &$subval) {
foreach ($subval->fields as $field) {
$subval->value = $this->setObjectProperty($subval->value, $field, $obj, $builder);
}
$subval = $subval->value;
}
$value = $value->data;
$obj->$key = $value;
}
if ($value instanceof ReplacableLink) {
foreach ($value->fields as $field) {
$value->value = $this->setObjectProperty($value->value, $field, $obj, $builder);
}
$obj->$key = $value->value;
}
}
return $obj;
}
/**
* @param object|array $obj
* @param string|int $key
* @param mixed $value
* @param \ReflectionClass $refl
* @param Builder $builder
* @return object|array
* @throws SampleException
*/
private function setObjectProperty($obj, $key, $value, Builder $builder)
{
if ($value instanceof ReplacableArray) {
foreach ($value->data as &$subval) {
foreach ($subval->fields as $field) {
$subval->value = $this->setObjectProperty($subval->value, $field, $obj, $builder);
}
$subval = $subval->value;
}
$value = $value->data;
}
if ($value instanceof ReplacableLink) {
foreach ($value->fields as $field) {
$value->value = $this->setObjectProperty($value->value, $field, $obj, $builder);
}
$value = $value->value;
}
try {
$this->accessor->setValue($obj, $key, $value);
} catch (NoSuchPropertyException $e) {
if (is_array($obj)) {
$obj[$key] = $value;
} elseif ($obj instanceof \stdClass) {
$obj->$key = $value;
} elseif ($builder->isStrict()) {
$type = get_class($obj);
throw new SampleException("Could not set value for '{$key}' in object of type '{$type}'", 0, $e);
} else {
$refl = new \ReflectionClass($builder->getClass());
if ($refl->hasProperty($key)) {
$prop = $refl->getProperty($key);
$prop->setAccessible(true);
$prop->setValue($obj, $value);
$prop->setAccessible(false);
} else {
$obj->{$key} = $value;
}
}
}
return $obj;
}
/**
* @param \ReflectionClass $refl
* @param Builder $builder
* @return object
* @throws SampleException
*/
private function constructObject(\ReflectionClass $refl, Builder $builder)
{
$constructor = $refl->getConstructor();
$obj = null;
if (null === $constructor || $constructor->getNumberOfParameters() === 0) {
$obj = $refl->newInstance();
} elseif ($constructor) {
if (!$builder->hasConstructor() && $constructor->getNumberOfRequiredParameters() === 0) {
$obj = $refl->newInstance();
} elseif ($builder->hasConstructor()) {
$args = $this->alignArgs($constructor, $this->sample($builder->constructor()), $builder->constructor());
$obj = $refl->newInstanceArgs($args);
}
}
if (null === $obj && !$builder->isStrict()) {
$obj = $refl->newInstanceWithoutConstructor();
} elseif (null === $obj) {
throw new SampleException("Constructor required for constructing {$builder->getClass()} in strict mode");
}
return $obj;
}
/**
* @param \ReflectionMethod $constructor
* @param array $data
* @return array
* @throws SampleException
*/
private function alignArgs(\ReflectionMethod $constructor, array $data, ConstructorBuilder $builder)
{
if ($builder->isPositional()) {
$aligned = $data;
} else {
$aligned = [];
foreach ($constructor->getParameters() as $param) {
if (isset($data[$param->getName()])) {
$aligned[] = $data[$param->getName()];
} else {
try {
$aligned[] = $param->getDefaultValue();
} catch (\ReflectionException $e) {
throw new SampleException("No value provided for argument {$param->getName()}", 0, $e);
}
}
}
}
if (count($aligned) < $constructor->getNumberOfRequiredParameters()) {
throw new SampleException("Not enough arguments provided for constructing the object");
}
return $aligned;
}
/**
* @return int
*/
public function getUniqueTries()
{
return $this->unique_tries;
}
/**
* Remove all unique tries.
*/
public function reset()
{
$this->sampled = [];
}
}