How to post Faker collection in Laravel testing? - php

I have a test case where the user will be allowed to create/post multiple items at once. Please take a look at my test:
/**
* Test multiple item creation.
*
* #return void
*/
public function testMultipleCreation()
{
$token = Test::generateToken();
$user = Test::getAuthenticatedUser();
$stall = factory(Stall::class)->make()->toArray();
$item = factory(Item::class, 5)->make()->toArray();
dump($item);
$user->addStall($stall);
$response = $this->withHeaders(['Authorization' => 'Bearer ' . $token]);
$response = $response->json('POST', route('items.store', $item));
$response->assertStatus(200);
// $this->assertDatabaseHas('items', $item);
}
and here is the controller:
/**
* Store a newly created resource in storage.
*
* #param \App\Http\Requests\StoreItem $request
* #return \App\Helpers\ResponseMessage
*/
public function store(StoreItem $request)
{
if (is_array($request)) {
$this->createMultiple($request);
}
$item = auth()->user()->addItem(
$request->validated()
);
return ResponseMessage::created('item', $item);
}
/**
* Create multiple items.
*
* #param array $items
* #return void
*/
protected function createMultiple($items)
{
$itemCollection = [];
foreach ($items as $item) {
$itemCollection[] = auth()->user()->addItem(
$item->validated()
);
}
return ResponseMessage::created('items', $itemCollection);
}
It works fine if I post it as $item = factory(Item::class)->make()->toArray();, but if I make it more than 1 factory item, it fails and throws this error:
1) Tests\Unit\ItemTest::testMultipleCreation
ErrorException: Array to string conversion
ERRORS!
Tests: 9, Assertions: 20, Errors: 1.

Problem is likely here
$response = $response->json('POST', route('items.store', $item));
Since $item is an Array, it doesn't know how to convert it.
Probably you meant
$response = $response->json('POST', route('items.store'), $item);

Related

Undefined array key "docScores"

I'm working with the scout library and the tntsearch driver but when I try to search for a word that is in the website I get the error in the title. The code part where the error is according to Laravel is the one below and the line is the one between **. Do you have any idea on how to solve this? Thanks!
public function map(Builder $builder, $results, $model)
{
if (empty($results['ids'])) {
return $model->newCollection([]);
}
$keys = collect($results['ids'])->values()->all();
$builder = $this->getBuilder($model);
if ($this->builder->queryCallback) {
call_user_func($this->builder->queryCallback, $builder);
}
$models = $builder->whereIn(
$model->getQualifiedKeyName(), $keys
)->get()->keyBy($model->getKeyName());
// sort models by user choice
if (!empty($this->builder->orders)) {
return $models->values();
}
// sort models by tnt search result set
return $model->newCollection(collect($results['ids'])->map(function ($hit) use ($models, $results) {
if (isset($models[$hit])) {
*return $models[$hit]->setAttribute('__tntSearchScore__', $results['docScores'][$hit]);*
}
})->filter()->all());
}
This is the whole page of code:
class TNTSearchEngine extends Engine
{
private $filters;
/**
* #var TNTSearch
*/
protected $tnt;
/**
* #var Builder
*/
protected $builder;
/**
* Create a new engine instance.
*
* #param TNTSearch $tnt
*/
public function __construct(TNTSearch $tnt)
{
$this->tnt = $tnt;
}
public function getTNT()
{
return $this->tnt;
}
/**
* Update the given model in the index.
*
* #param Collection $models
*
* #return void
*/
public function update($models)
{
$this->initIndex($models->first());
$this->tnt->selectIndex("{$models->first()->searchableAs()}.index");
$index = $this->tnt->getIndex();
$index->setPrimaryKey($models->first()->getKeyName());
$index->indexBeginTransaction();
$models->each(function ($model) use ($index) {
$array = $model->toSearchableArray();
if (empty($array)) {
return;
}
if ($model->getKey()) {
$index->update($model->getKey(), $array);
} else {
$index->insert($array);
}
});
$index->indexEndTransaction();
}
/**
* Remove the given model from the index.
*
* #param Collection $models
*
* #return void
*/
public function delete($models)
{
$this->initIndex($models->first());
$models->each(function ($model) {
$this->tnt->selectIndex("{$model->searchableAs()}.index");
$index = $this->tnt->getIndex();
$index->setPrimaryKey($model->getKeyName());
$index->delete($model->getKey());
});
}
/**
* Perform the given search on the engine.
*
* #param Builder $builder
*
* #return mixed
*/
public function search(Builder $builder)
{
try {
return $this->performSearch($builder);
} catch (IndexNotFoundException $e) {
$this->initIndex($builder->model);
}
}
/**
* Perform the given search on the engine.
*
* #param Builder $builder
* #param int $perPage
* #param int $page
*
* #return mixed
*/
public function paginate(Builder $builder, $perPage, $page)
{
$results = $this->performSearch($builder);
if ($builder->limit) {
$results['hits'] = $builder->limit;
}
$filtered = $this->discardIdsFromResultSetByConstraints($builder, $results['ids']);
$results['hits'] = $filtered->count();
$chunks = array_chunk($filtered->toArray(), $perPage);
if (empty($chunks)) {
return $results;
}
if (array_key_exists($page - 1, $chunks)) {
$results['ids'] = $chunks[$page - 1];
} else {
$results['ids'] = [];
}
return $results;
}
/**
* Perform the given search on the engine.
*
* #param Builder $builder
*
* #return mixed
*/
protected function performSearch(Builder $builder, array $options = [])
{
$index = $builder->index ?: $builder->model->searchableAs();
$limit = $builder->limit ?: 10000;
$this->tnt->selectIndex("{$index}.index");
$this->builder = $builder;
if (isset($builder->model->asYouType)) {
$this->tnt->asYouType = $builder->model->asYouType;
}
if ($builder->callback) {
return call_user_func(
$builder->callback,
$this->tnt,
$builder->query,
$options
);
}
$builder->query = $this->applyFilters('query_expansion', $builder->query, get_class($builder->model));
if (isset($this->tnt->config['searchBoolean']) ? $this->tnt->config['searchBoolean'] : false) {
$res = $this->tnt->searchBoolean($builder->query, $limit);
event(new SearchPerformed($builder, $res, true));
return $res;
} else {
$res = $this->tnt->search($builder->query, $limit);
event(new SearchPerformed($builder, $res));
return $res;
}
}
/**
* Map the given results to instances of the given model.
*
* #param mixed $results
* #param \Illuminate\Database\Eloquent\Model $model
*
* #return Collection
*/
public function map(Builder $builder, $results, $model)
{
if (empty($results['ids'])) {
return $model->newCollection([]);
}
$keys = collect($results['ids'])->values()->all();
$builder = $this->getBuilder($model);
if ($this->builder->queryCallback) {
call_user_func($this->builder->queryCallback, $builder);
}
$models = $builder->whereIn(
$model->getQualifiedKeyName(), $keys
)->get()->keyBy($model->getKeyName());
// sort models by user choice
if (!empty($this->builder->orders)) {
return $models->values();
}
// sort models by tnt search result set
return $model->newCollection(collect($results['ids'])->map(function ($hit) use ($models, $results) {
if (isset($models[$hit])) {
return $models[$hit]->setAttribute('__tntSearchScore__', $results['docScores'][$hit]);
}
})->filter()->all());
}
/**
* Map the given results to instances of the given model via a lazy collection.
*
* #param mixed $results
* #param \Illuminate\Database\Eloquent\Model $model
*
* #return LazyCollection
*/
public function lazyMap(Builder $builder, $results, $model)
{
if (empty($results['ids'])) {
return LazyCollection::make();
}
$keys = collect($results['ids'])->values()->all();
$builder = $this->getBuilder($model);
if ($this->builder->queryCallback) {
call_user_func($this->builder->queryCallback, $builder);
}
$models = $builder->whereIn(
$model->getQualifiedKeyName(), $keys
)->get()->keyBy($model->getKeyName());
// sort models by user choice
if (!empty($this->builder->orders)) {
return $models->values();
}
// sort models by tnt search result set
return $model->newCollection($results['ids'])->map(function ($hit) use ($models) {
if (isset($models[$hit])) {
return $models[$hit];
}
})->filter()->values();
}
/**
* Return query builder either from given constraints, or as
* new query. Add where statements to builder when given.
*
* #param \Illuminate\Database\Eloquent\Model $model
*
* #return Builder
*/
public function getBuilder($model)
{
// get query as given constraint or create a new query
$builder = isset($this->builder->constraints) ? $this->builder->constraints : $model->newQuery();
$builder = $this->handleSoftDeletes($builder, $model);
$builder = $this->applyWheres($builder);
$builder = $this->applyOrders($builder);
return $builder;
}
/**
* Pluck and return the primary keys of the given results.
*
* #param mixed $results
* #return \Illuminate\Support\Collection
*/
public function mapIds($results)
{
if (empty($results['ids'])) {
return collect();
}
return collect($results['ids'])->values();
}
/**
* Get the total count from a raw result returned by the engine.
*
* #param mixed $results
*
* #return int
*/
public function getTotalCount($results)
{
return $results['hits'];
}
public function initIndex($model)
{
$indexName = $model->searchableAs();
if (!file_exists($this->tnt->config['storage']."/{$indexName}.index")) {
$indexer = $this->tnt->createIndex("$indexName.index");
$indexer->setDatabaseHandle($model->getConnection()->getPdo());
$indexer->setPrimaryKey($model->getKeyName());
}
}
/**
* The search index results ($results['ids']) need to be compared against our query
* that contains the constraints.
*
* To get the correct results and counts for the pagination, we remove those ids
* from the search index results that were found by the search but are not part of
* the query ($sub) that is constrained.
*
* This is achieved with self joining the constrained query as subquery and selecting
* the ids which are not matching to anything (i.e., is null).
*
* The constraints usually remove only a small amount of results, which is why the non
* matching results are looked up and removed, instead of returning a collection with
* all the valid results.
*/
private function discardIdsFromResultSetByConstraints($builder, $searchResults)
{
$qualifiedKeyName = $builder->model->getQualifiedKeyName(); // tableName.id
$subQualifiedKeyName = 'sub.'.$builder->model->getKeyName(); // sub.id
$sub = $this->getBuilder($builder->model)->whereIn(
$qualifiedKeyName, $searchResults
); // sub query for left join
$discardIds = $builder->model->newQuery()
->select($qualifiedKeyName)
->leftJoin(DB::raw('('.$sub->getQuery()->toSql().') as '.$builder->model->getConnection()->getTablePrefix().'sub'), $subQualifiedKeyName, '=', $qualifiedKeyName)
->addBinding($sub->getQuery()->getBindings(), 'join')
->whereIn($qualifiedKeyName, $searchResults)
->whereNull($subQualifiedKeyName)
->pluck($builder->model->getKeyName());
// returns values of $results['ids'] that are not part of $discardIds
return collect($searchResults)->diff($discardIds);
}
/**
* Determine if the given model uses soft deletes.
*
* #param \Illuminate\Database\Eloquent\Model $model
* #return bool
*/
protected function usesSoftDelete($model)
{
return in_array(SoftDeletes::class, class_uses_recursive($model));
}
/**
* Determine if soft delete is active and depending on state return the
* appropriate builder.
*
* #param Builder $builder
* #param \Illuminate\Database\Eloquent\Model $model
* #return Builder
*/
private function handleSoftDeletes($builder, $model)
{
// remove where statement for __soft_deleted when soft delete is not active
// does not show soft deleted items when trait is attached to model and
// config('scout.soft_delete') is false
if (!$this->usesSoftDelete($model) || !config('scout.soft_delete', true)) {
unset($this->builder->wheres['__soft_deleted']);
return $builder;
}
/**
* Use standard behaviour of Laravel Scout builder class to support soft deletes.
*
* When no __soft_deleted statement is given return all entries
*/
if (!array_key_exists('__soft_deleted', $this->builder->wheres)) {
return $builder->withTrashed();
}
/**
* When __soft_deleted is 1 then return only soft deleted entries
*/
if ($this->builder->wheres['__soft_deleted']) {
$builder = $builder->onlyTrashed();
}
/**
* Returns all undeleted entries, default behaviour
*/
unset($this->builder->wheres['__soft_deleted']);
return $builder;
}
/**
* Apply where statements as constraints to the query builder.
*
* #param Builder $builder
* #return \Illuminate\Support\Collection
*/
private function applyWheres($builder)
{
// iterate over given where clauses
return collect($this->builder->wheres)->map(function ($value, $key) {
// for reduce function combine key and value into array
return [$key, $value];
})->reduce(function ($builder, $where) {
// separate key, value again
list($key, $value) = $where;
return $builder->where($key, $value);
}, $builder);
}
/**
* Apply order by statements as constraints to the query builder.
*
* #param Builder $builder
* #return \Illuminate\Support\Collection
*/
private function applyOrders($builder)
{
//iterate over given orderBy clauses - should be only one
return collect($this->builder->orders)->map(function ($value, $key) {
// for reduce function combine key and value into array
return [$value["column"], $value["direction"]];
})->reduce(function ($builder, $orderBy) {
// separate key, value again
list($column, $direction) = $orderBy;
return $builder->orderBy($column, $direction);
}, $builder);
}
/**
* Flush all of the model's records from the engine.
*
* #param \Illuminate\Database\Eloquent\Model $model
* #return void
*/
public function flush($model)
{
$indexName = $model->searchableAs();
$pathToIndex = $this->tnt->config['storage']."/{$indexName}.index";
if (file_exists($pathToIndex)) {
unlink($pathToIndex);
}
}
/**
* Create a search index.
*
* #param string $name
* #param array $options
* #return mixed
*
* #throws \Exception
*/
public function createIndex($name, array $options = [])
{
throw new Exception('TNT indexes are created automatically upon adding objects.');
}
/**
* Delete a search index.
*
* #param string $name
* #return mixed
*/
public function deleteIndex($name)
{
throw new Exception(sprintf('TNT indexes cannot reliably be removed. Please manually remove the file in %s/%s.index', config('scout.tntsearch.storage'), $name));
}
/**
* Adds a filter
*
* #param string
* #param callback
* #return void
*/
public function addFilter($name, $callback)
{
if (!is_callable($callback, true)) {
throw new InvalidArgumentException(sprintf('Filter is an invalid callback: %s.', print_r($callback, true)));
}
$this->filters[$name][] = $callback;
}
/**
* Returns an array of filters
*
* #param string
* #return array
*/
public function getFilters($name)
{
return isset($this->filters[$name]) ? $this->filters[$name] : [];
}
/**
* Returns a string on which a filter is applied
*
* #param string
* #param string
* #return string
*/
public function applyFilters($name, $result, $model)
{
foreach ($this->getFilters($name) as $callback) {
// prevent fatal errors, do your own warning or
// exception here as you need it.
if (!is_callable($callback)) {
continue;
}
$result = call_user_func($callback, $result, $model);
}
return $result;
}
}

Best way to create associative array of unique objects in PHP?

I'm trying to create a list of unique objects in PHP.
I want to access each object by a unique name but I also need the object to know its name.
My idea was to create an associative array of objects like this:
class MyObject()
{
public $name;
public $property1;
public $property2;
function __construct($name)
{
$this->name = $name;
}
function doSomething()
{
echo $name.$property1;
}
function doSomethingElse()
{
echo $name.$property2;
}
}
$array = array();
$name = 'example';
$array[$name] = new MyObject($name);
$array[$name]->property1 = 'xyz';
$array[$name]->property2 = 123;
$name = 'test';
$array[$name] = new MyObject($uniqueName);
$array[$name]->property1 = 'abc';
$array[$name]->property2 = 321;
$array['example']->doSomething();
$array['test']->doSomethingElse();
I'm pretty new to PHP and coding in general and I feel like this is a really stupid solution so I wanted to ask if you know any better ways of doing this.
For a simple use-case you've provided, perhaps implementing the predefined interface ArrayAccess may fit the bill. Something like:
class UniqueCollection implements ArrayAccess
{
/**
* #var array<string, MyObject>
*/
private $collection = [];
/**
* #param mixed $offset
*
* #return bool
*/
public function offsetExists($offset): bool
{
return isset($this->collection[$offset]);
}
/**
* #param mixed $offset
*
* #return MyObject|null
*/
public function offsetGet($offset): mixed
{
return $this->collection[$offset] ?? null;
}
/**
* #param mixed $offset
* #param array<mixed, mixed> $values
*
* #return void
*/
public function offsetSet($offset, $values): void
{
if ($offset === null) {
// Error
}
// Uncomment if we don't want to overwrite existing MyObject with the same name,
// eg, throw exception
// if (isset($this->collection[$offset]) {
// // Error
// }
$count($values);
if ($count !== 0 || $count !== 2) {
// Error
}
$myObj = new MyObject($offset);
if ($count) {
// Fetch the values only so we only have to deal with zero-based integer offsets
$values = array_values($values);
// Establish a convention on which index goes to what property
$myObj->property1 = $values[0];
$myObj->property2 = $values[1];
}
$this->collection[$offset] = $myObj;
}
/**
* #param mixed $offset
*
* #return void
*/
public function offsetUnset($offset): void
{
return unset($this->collection[$offset]);
}
}
Use:
$set = new UniqueCollection();
$set['example'] = ['xyz', 123];
$set['test'] = []; // not exactly elegent, but hey ¯\_(ツ)_/¯
$set['test']->property1 = 'abc'; // should work fine, unless we clone the MyObject internally
$set['test']->property2 = 321; // ditto
$set['example']->doSomething();
$set['test']->doSomethingElse();
I haven't tested this so let me know if there are any issues.

Doctrine 3 merge alternantive

I am currently using doctrine merge to "restore" an entity with relationships after retrieving it from the session.
As from doctrine 3, this function will be deprecated so I am wondering if there is any way to keep an entity object in the session for a while before persisting it to the database.
I need this for a multistep form through which my object gets populated.
For now, the only solution i see is storing the entity in a temporary database table but i don't really like this idea because my table will be filled with "junk".
Thanks !
There are two ways that I have found.
The first is to create your own implementation. I have go this way because there were a lot of usages of merge in project. It looks hacky, but works:
class DoctrineMergeService
{
/**
* #var EntityManager
*/
private $em;
/**
* #param EntityManager $em
*/
public function __construct(EntityManager $em)
{
$this->em = $em;
}
/**
* #param object $entity
*
* #return object
*
* #throws \Doctrine\ORM\ORMException
* #throws \Doctrine\ORM\OptimisticLockException
* #throws \Doctrine\ORM\TransactionRequiredException
*/
public function merge(object $entity): object
{
$mergedEntity = null;
$className = get_class($entity);
$identifiers = $this->getIdentifiersFromEntity($entity);
$entityFromDoctrine = $this->em->find($className, $identifiers);
if ($entityFromDoctrine) {
$mergedEntity = $this->mergeEntities($entityFromDoctrine, $entity);
} else {
$this->em->persist($entity);
$mergedEntity = $entity;
}
return $mergedEntity;
}
/**
* #param object $entity
*
* #return array
*/
private function getIdentifiersFromEntity(object $entity): array
{
$className = get_class($entity);
$meta = $this->em->getClassMetadata($className);
$identifiers = $meta->getIdentifierValues($entity);
return $identifiers;
}
/**
* #param object $first
* #param object $second
*
* #return object
*/
private function mergeEntities(object $first, object $second): object
{
$classNameFirst = get_class($first);
$metaFirst = $this->em->getClassMetadata($classNameFirst);
$classNameSecond = get_class($second);
$metaSecond = $this->em->getClassMetadata($classNameSecond);
$fieldNames = $metaFirst->getFieldNames();
foreach ($fieldNames as $fieldName) {
$secondValue = $metaSecond->getFieldValue($second, $fieldName);
$metaFirst->setFieldValue($first, $fieldName, $secondValue);
}
return $first;
}
}
The second is to use serializer, not tested:
// this is controller or something like controller
public function save($id)
{
$serializedJsonFromSession = $this->session->get('serialized_json');
$doctrine = $this->getDoctrine();
$entity = $doctrine->getRepository(Entity::class)->find($id);
if (!$entity) {
$entity = new Entity();
$doctrine->persist($entity);
}
$serializer->deserialize(
$serializedJsonFromSession,
Entity::class,
'json',
[AbstractNormalizer::OBJECT_TO_POPULATE => $entity]
);
$doctrine->flush();
}

Laravel 5.2 Builder insert replace multiple rows

I have this code, can insert an array at first time, but when trying replace and update, it returns error
Integrity constraint violation: 1062 Duplicate entry for composite key ['periodo_id','asociado_id']
Model:
<?php
namespace App;
use Illuminate\Database\Eloquent\Model;
class Lectura extends Model
{
protected $primaryKey = array('periodo_id', 'asociado_id');
public $timestamps = false;
public $incrementing = false;
}
And controller:
$rows = DB::table('lecturas_temp')->get();
$arr = array();
foreach ($rows as $row) {
$a = [
'asociado_id' => $row->asociado_id,
'periodo_id' => $request->periodo_id,
'nombre' => $row->nombre,
];
array_push($arr, $a);
}
DB::table('lecturas')->insert($arr);
Any alternatives to line DB::table('lecturas')->insert($arr)?
I tried Eloquent Lectura::insert($arr) and updateOrCreate but same results;
The error message is crystal clear. You are using a composite key ['periodo_id', 'asociado_id'], which means you cannot insert the same data twice because you defined it as your primary key.
If you are expecting to have duplicate composite key, then please, remove it from your model as the primary key. However, if you want the data to be unique, you should use updateOrCreate().
$rows = DB::table('lecturas_temp')->get();
$arr = array();
foreach ($rows as $row) {
Lectura::updateOrCreate(
[
'asociado_id' => $row->asociado_id,
'periodo_id' => $request->periodo_id
],
[
'nombre' => $row->nombre
]
);
}
As you can see, updateOrCreate() takes 2 arrays as arguments. The first array is the elements used to verify if it already exists. Also, unfortunately you will have to do it one by one instead all in one go as you were doing.
If you want to stick to the query builder, you can use DB::updateOrInsert(), with the same call signature (passing 2 arrays).
Using this code I did solved: https://github.com/yadakhov/insert-on-duplicate-key
class Lectura extends Model
{
protected $primaryKey = array('periodo_id', 'asociado_id');
public $timestamps = false;
public $incrementing = false;
public static function insertOnDuplicateKey(array $data, array $updateColumns = null)
{
if (empty($data)) {
return false;
}
// Case where $data is not an array of arrays.
if (!isset($data[0])) {
$data = [$data];
}
$sql = static::buildInsertOnDuplicateSql($data, $updateColumns);
$data = static::inLineArray($data);
return self::getModelConnectionName()->affectingStatement($sql, $data);
}
/**
* Insert using mysql INSERT IGNORE INTO.
*
* #param array $data
*
* #return int 0 if row is ignored, 1 if row is inserted
*/
public static function insertIgnore(array $data)
{
if (empty($data)) {
return false;
}
// Case where $data is not an array of arrays.
if (!isset($data[0])) {
$data = [$data];
}
$sql = static::buildInsertIgnoreSql($data);
$data = static::inLineArray($data);
return self::getModelConnectionName()->affectingStatement($sql, $data);
}
/**
* Insert using mysql REPLACE INTO.
*
* #param array $data
*
* #return int 1 if row is inserted without replacements, greater than 1 if rows were replaced
*/
public static function replace(array $data)
{
if (empty($data)) {
return false;
}
// Case where $data is not an array of arrays.
if (!isset($data[0])) {
$data = [$data];
}
$sql = static::buildReplaceSql($data);
$data = static::inLineArray($data);
return self::getModelConnectionName()->affectingStatement($sql, $data);
}
/**
* Static function for getting table name.
*
* #return string
*/
public static function getTableName()
{
$class = get_called_class();
return (new $class())->getTable();
}
/**
* Static function for getting connection name
*
* #return string
*/
public static function getModelConnectionName()
{
$class = get_called_class();
return (new $class())->getConnection();
}
/**
* Get the table prefix.
*
* #return string
*/
public static function getTablePrefix()
{
return self::getModelConnectionName()->getTablePrefix();
}
/**
* Static function for getting the primary key.
*
* #return string
*/
public static function getPrimaryKey()
{
$class = get_called_class();
return (new $class())->getKeyName();
}
/**
* Build the question mark placeholder. Helper function for insertOnDuplicateKeyUpdate().
* Helper function for insertOnDuplicateKeyUpdate().
*
* #param $data
*
* #return string
*/
protected static function buildQuestionMarks($data)
{
$lines = [];
foreach ($data as $row) {
$count = count($row);
$questions = [];
for ($i = 0; $i < $count; ++$i) {
$questions[] = '?';
}
$lines[] = '(' . implode(',', $questions) . ')';
}
return implode(', ', $lines);
}
/**
* Get the first row of the $data array.
*
* #param array $data
*
* #return mixed
*/
protected static function getFirstRow(array $data)
{
if (empty($data)) {
throw new \InvalidArgumentException('Empty data.');
}
list($first) = $data;
if (!is_array($first)) {
throw new \InvalidArgumentException('$data is not an array of array.');
}
return $first;
}
/**
* Build a value list.
*
* #param array $first
*
* #return string
*/
protected static function getColumnList(array $first)
{
if (empty($first)) {
throw new \InvalidArgumentException('Empty array.');
}
return '`' . implode('`,`', array_keys($first)) . '`';
}
/**
* Build a value list.
*
* #param array $first
*
* #return string
*/
protected static function buildValuesList(array $first)
{
$out = [];
foreach (array_keys($first) as $key) {
$out[] = sprintf('`%s` = VALUES(`%s`)', $key, $key);
}
return implode(', ', $out);
}
/**
* Inline a multiple dimensions array.
*
* #param $data
*
* #return array
*/
protected static function inLineArray(array $data)
{
return call_user_func_array('array_merge', array_map('array_values', $data));
}
/**
* Build the INSERT ON DUPLICATE KEY sql statement.
*
* #param array $data
* #param array $updateColumns
*
* #return string
*/
protected static function buildInsertOnDuplicateSql(array $data, array $updateColumns = null)
{
$first = static::getFirstRow($data);
$sql = 'INSERT INTO `' . static::getTablePrefix() . static::getTableName() . '`(' . static::getColumnList($first) . ') VALUES' . PHP_EOL;
$sql .= static::buildQuestionMarks($data) . PHP_EOL;
$sql .= 'ON DUPLICATE KEY UPDATE ';
if (empty($updateColumns)) {
$sql .= static::buildValuesList($first);
} else {
$sql .= static::buildValuesList(array_combine($updateColumns, $updateColumns));
}
return $sql;
}
/**
* Build the INSERT IGNORE sql statement.
*
* #param array $data
*
* #return string
*/
protected static function buildInsertIgnoreSql(array $data)
{
$first = static::getFirstRow($data);
$sql = 'INSERT IGNORE INTO `' . static::getTablePrefix() . static::getTableName() . '`(' . static::getColumnList($first) . ') VALUES' . PHP_EOL;
$sql .= static::buildQuestionMarks($data);
return $sql;
}
/**
* Build REPLACE sql statement.
*
* #param array $data
*
* #return string
*/
protected static function buildReplaceSql(array $data)
{
$first = static::getFirstRow($data);
$sql = 'REPLACE INTO `' . static::getTablePrefix() . static::getTableName() . '`(' . static::getColumnList($first) . ') VALUES' . PHP_EOL;
$sql .= static::buildQuestionMarks($data);
return $sql;
}
In Controller
Lectura::replace($arr);

Access array using dynamic path

I have an issue in accessing the array in php.
$path = "['a']['b']['c']";
$value = $array.$path;
In the above piece of code I have an multidimensional array named $array.
$path is a dynamic value which I would get from database.
Now I want to retrieve the value from $array using $path but I am not able to.
$value = $array.$path
returns me
Array['a']['b']['c']
rather than the value.
I hope I have explained my question properly.
You have two options. First (evil) if to use eval() function - i.e. interpret your string as code.
Second is to parse your path. That will be:
//$path = "['a']['b']['c']";
preg_match_all("/\['(.*?)'\]/", $path, $rgMatches);
$rgResult = $array;
foreach($rgMatches[1] as $sPath)
{
$rgResult=$rgResult[$sPath];
}
The Kohana framework "Arr" class (API) has a method (Arr::path) that does something similar to what you are requesting. It simply takes an array and a path (with a . as delimiter) and returns the value if found. You could modify this method to suit your needs.
public static function path($array, $path, $default = NULL, $delimiter = NULL)
{
if ( ! Arr::is_array($array))
{
// This is not an array!
return $default;
}
if (is_array($path))
{
// The path has already been separated into keys
$keys = $path;
}
else
{
if (array_key_exists($path, $array))
{
// No need to do extra processing
return $array[$path];
}
if ($delimiter === NULL)
{
// Use the default delimiter
$delimiter = Arr::$delimiter;
}
// Remove starting delimiters and spaces
$path = ltrim($path, "{$delimiter} ");
// Remove ending delimiters, spaces, and wildcards
$path = rtrim($path, "{$delimiter} *");
// Split the keys by delimiter
$keys = explode($delimiter, $path);
}
do
{
$key = array_shift($keys);
if (ctype_digit($key))
{
// Make the key an integer
$key = (int) $key;
}
if (isset($array[$key]))
{
if ($keys)
{
if (Arr::is_array($array[$key]))
{
// Dig down into the next part of the path
$array = $array[$key];
}
else
{
// Unable to dig deeper
break;
}
}
else
{
// Found the path requested
return $array[$key];
}
}
elseif ($key === '*')
{
// Handle wildcards
$values = array();
foreach ($array as $arr)
{
if ($value = Arr::path($arr, implode('.', $keys)))
{
$values[] = $value;
}
}
if ($values)
{
// Found the values requested
return $values;
}
else
{
// Unable to dig deeper
break;
}
}
else
{
// Unable to dig deeper
break;
}
}
while ($keys);
// Unable to find the value requested
return $default;
}
I was hoping to find an elegant solution to nested array access without throwing undefined index errors, and this post hits high on google. I'm late to the party, but I wanted to weigh in for future visitors.
A simple isset($array['a']['b']['c'] can safely check nested values, but you need to know the elements to access ahead of time. I like the dot notation for accessing multidimensional arrays, so I wrote a class of my own. It does require PHP 5.6.
This class parses a string path written in dot-notation and safely accesses the nested values of the array or array-like object (implements ArrayAccess). It will return the value or NULL if not set.
use ArrayAccess;
class SafeArrayGetter implements \JsonSerializable {
/**
* #var array
*/
private $data;
/**
* SafeArrayGetter constructor.
*
* #param array $data
*/
public function __construct( array $data )
{
$this->data = $data;
}
/**
* #param array $target
* #param array ...$indices
*
* #return array|mixed|null
*/
protected function safeGet( array $target, ...$indices )
{
$movingTarget = $target;
foreach ( $indices as $index )
{
$isArray = is_array( $movingTarget ) || $movingTarget instanceof ArrayAccess;
if ( ! $isArray || ! isset( $movingTarget[ $index ] ) ) return NULL;
$movingTarget = $movingTarget[ $index ];
}
return $movingTarget;
}
/**
* #param array ...$keys
*
* #return array|mixed|null
*/
public function getKeys( ...$keys )
{
return static::safeGet( $this->data, ...$keys );
}
/**
* <p>Access nested array index values by providing a dot notation access string.</p>
* <p>Example: $safeArrayGetter->get('customer.paymentInfo.ccToken') ==
* $array['customer']['paymentInfo']['ccToken']</p>
*
* #param $accessString
*
* #return array|mixed|null
*/
public function get( $accessString )
{
$keys = $this->parseDotNotation( $accessString );
return $this->getKeys( ...$keys );
}
/**
* #param $string
*
* #return array
*/
protected function parseDotNotation( $string )
{
return explode( '.', strval( $string ) );
}
/**
* #return array
*/
public function toArray()
{
return $this->data;
}
/**
* #param int $options
* #param int $depth
*
* #return string
*/
public function toJson( $options = 0, $depth = 512 )
{
return json_encode( $this, $options, $depth );
}
/**
* #param array $data
*
* #return static
*/
public static function newFromArray( array $data )
{
return new static( $data );
}
/**
* #param \stdClass $data
*
* #return static
*/
public static function newFromObject( \stdClass $data )
{
return new static( json_decode( json_encode( $data ), TRUE ) );
}
/**
* Specify data which should be serialized to JSON
* #link http://php.net/manual/en/jsonserializable.jsonserialize.php
* #return array data which can be serialized by <b>json_encode</b>,
* which is a value of any type other than a resource.
* #since 5.4.0
*/
function jsonSerialize()
{
return $this->toArray();
}
}

Categories