I have a multidimensional array in php, which I want to manipulate according to some rules.
Input-Array:
(Variable printed as JSON)
[
{
"meta":{
"title": "Adressdata",
"name": "adress"
},
"data":{
"desc":{ "tooltip": "text",
"maxlength": "100"
},
"line1":{ "tooltip": "Recipient",
"maxlength": "40"
}
}
]
Rules:
{
"0>meta>title": "Companyaddress",
"0>data>desc": {
"tooltip": "Another Text",
"maxLength": "150"
}
}
(There are 2 rules, the index explains which field from the input-array has to be changed, and the value contains the data to be inserted instead. Note that the second rule wants to insert an object)
Problem:
I want to change the content of the input-array according to the rules, but I struggle in doing that.
Here's the php-Code I already have:
<?php
$input = json_decode(file_get_contents("inputData.json"),true);
$rules = json_decode(file_get_contents("rules.json"),true);
foreach ($rules as $target => $replacement) {
//rule: "0>meta>title": "newTitle"
$node = $input;
foreach (explode(">",$target) as $index) {
$node = $node[$index];
}
$node = $replacement; //replace doesn't work
}
echo "Result-Array: ";
print_r($input);
?>
Everything works, in except that I can't change the values.
Why? Because everytime I set $node-Variable, I create a new Variable.
And I only change the content of $node, but not the content of $input.
So I tried to use references - which doesn't work either:
When I change the 2 $node-Lines to this code:
$node = &$input;
[...]
$node = &$node[$keys[$i]]; //go to child-node
But this doesn't work, because the second line, where I just want to navigate to a child-node, changes the parent-element (because $node is a reference).
I have no idea if there's a trick to do that, I hope someone can help me
Okay, here's some decision, but you should change $rules structure, it should be the same as $input:
$input = json_decode('[{"meta":{"title": "Adressdata","name": "adress"},"data":{"desc":{"tooltip":"text","maxlength":"100"},"line1":{"tooltip":"Recipient","maxlength":"40"}}}]',true);
$rules = json_decode('[{"meta":{"title": "Companyaddress"},"data":{"desc":{"tooltip":"Another Text","maxlength":"150"}}}]',true);
// if you print both arrays you will see that they have the similar structure
// but $rules consists only of items that you need to change.
// next:
echo'<pre>',print_r(array_replace_recursive($input, $rules)),'</pre>';
// BINGO!
Ok, after some frustrating hours, I finally found a way to achieve this.
This is the function I wrote:
/**
* Manipulates an array according to the given rules
*/
function manipulateData($input, $manipulations){
foreach ($manipulations as $target => $replacement) { //for each rule
$tmpRefNum = 0; //Variable-Nameprefix for the reference (don't generate random names)
$node = "node".(++$tmpRefNum); //"node1"
$$node = &$input; //$node1 --> $input
foreach (explode(">",$target) as $index) { //for each child in the rule
$parent = &$$node; //We search for a child. So the old node becomes a parent
if(!isset($parent[$index])){ //Does the child exist? no --> create missing child
$parent[$index] = ""; //give birth! (value will be set later)
}
$node = "node".(++$tmpRefNum); //Generate a new reference-Name: "node2", "node3", ...
$$node = &$parent[$index]; //Point to child
}
$$node = $replacement; //Change the child-content
//Unset all generated references (just to not spam the variables)
/* */ for($i=1;$i<=$tmpRefNum;$i++){
/* */ $node = "node".$i;
/* */ unset($$node);
/* */ }
/* */
}
return $input;
}
Description:
The solution is, that I just generate a new variable/reference for each child-node I find. In order to do this, I need the $$-Syntax provided by php.
It's a bit dirty, because I have a variable-factory, so if someone finds a better solution it would be awesome.
It would be great, if I would get some feedback if this is a good solution or not.
Related
I'm Trying to create single array that contains all the ID of parent and child from the database.
But all I was getting is single data.
My ideal output is:
array('160', '161', '162', '163', '164');
what am I getting is only
array('160');
Here is what I've done so far.
public function arrayId(array $elements) {
$where_in = array();
foreach($elements as $element){
if($element->isArray) {
$elems = $this->my_model->select_where('tbl_policies', array('parent_id' => $element->id));
$this->arrayId($elems);
}
$where_in[] = $element->id;
}
return $where_in;
}
$id = 160; //for instance
$elements = $this->my_model->select_where('tbl_policies', array('id' => $id));
$where_in = $this->arrayId($elements);
die(print_r($where_in));
and the data I'm fetching here:
tbl_policies
It's kinda difficult for me to construct questions. So please if something is not clear, do comment below, I'll try my best to make it more understandable. Thanks in advance.
I understand, that you want to delete a parent with all its children and grandchildren. But you do it not directly and sequentially rather want to collect all ids of the records to be deleted. You should go following steps:
Parent-Id (example 160) is already known. Add this to your list.
Write a recursive function such as getChildrenIds(parentId).
Within this function you should iterate over children. And if a child has the flag "isArray" (according to your application logic) then you should call getChildrenIds(currentChildId)
I have written following function. It should work.
public function getChildrenIds( int $parentId, array &$idList) {
$idList[] = $parentId;
$records = $this->my_model->select_where('tbl_policies', array('parent_id' => $parentId));
foreach($records as $r){
if($r->isArray)
$this->getChildrenIds($r->id, $idList);
else
$idList[] = $r->id;
}
return;
}
public function CollectIds(){
$id = 160; //for instance
$where_in = array();
$this->getChildrenIds($id, $where_in);
}
Please notice, that $where_in passed by reference to the recursive function getChildrenIds() and filled there.
Given a JSON parsed object with deep nesting, I would like to extract an (array of) image(s) from a nested structure like this:
object: {
type: "...",
title:"...",
description: {
image:[
src:"logo1.png",
...:...
]
},
somethingelse: {
deeper:[
{imageurl:"logo2.jpg"}
]
}
}
how would I create a function that returns an array of images like this?
$images = getAllImagesFromObject(json_parse($jsonstring));
I do not know beforehand how deep the nesting will be and what the key will be, just any string beginning with http and ending on jpg, png and gif would be useful
I do not have any example since I do not know which method to use and I do not care what the key is so some var dump is also ok.
Perhaps even regex the jsonstring for "http://....[jpg|gif|png]" would be a solution
Something like this should do the trick. I did not test though.
function getAllImagesFromObject($obj) {
$result = array();
foreach ($obj as $prop)
if (is_array($prop) || is_object($prop))
$result = array_merge($result, getAllImagesFromObject($prop));
else if (preg_match('/\.(jpg|jpeg|gif|png|bmp)$/', $prop))
$result[] = $prop;
return $result;
}
I am reading in a an xml file which returns me a SimpleXMLElement Object representation of the xml. I am going to take an array and feed in new values to that object. I don't know what I am going to be in that array.
if I were to brute force this I would do something like this.
//Solution 1: Brute Force
//Just creating an array and value for purposes of demonstration.
$arOfData = array( [0]=>"theFirstNode", [1]=>"theSecondNode",[2]=>"theThirdNode" );
$value = "The XML Node Value";
$simpleXml->$arOfData[0]->$arOfData[1]->$arOfData[2] = $value;
//The next best thing I can think of doing is something like this.
//Solution 2: Semi-brute force
//
foreach($this->arrayData as $key => $value) {
$xmlNodes = explode( '-', $key);
$numNodes = count($xmlNodes);
switch($numNodes) {
case 1:
$simpleXml->$xmlNodes[0] = $value;
break;
case 2:
$simpleXml->$xmlNodes[0]->$xmlNodes[1] = $value;
break;
case 3:
$simpleXml->$xmlNodes[0]->$xmlNodes[1]->$xmlNodes[2] = $value;
break;
case 4:
$simpleXml->$xmlNodes[0]->$xmlNodes[1]->$xmlNodes[2]->$xmlNodes[3] = $value;
break;
case 5:
$simpleXml->$xmlNodes[0]->$xmlNodes[1]->$xmlNodes[2]->$xmlNodes[3]->$xmlNodes[4] = $value;
break;
}
}
*note This solution uses the array key and explodes it to an array delimited by a dash and then uses the array value as the new xml value. So don't let that distract you.
The problem with solution #2 is: what happens when we get a xml node that is deeper than 5? Its not going to be stuffed into our new object we are creating. Oh oh. It's also not very elegant ;). I am not sure how to do this in a more recursive manner.
Like you already wrote in your question, you need to have this dynamically because you do not know about the number of parent elements.
You need to dig a little deeper into how simpexml works to get this done.
But first let me suggest you to have a different notation, not with the minus sign you have but with a slash like in a path.
first/second/third
This is also common with Xpath and I think it's pretty well speaking for itself. Also the minus sign can be part of an element name, but the slash can not. So this is just a bit better.
Before I show you how you can easily access that <third> element node to set its value, first lets look at some assignment basics in simplexml.
To access and set this element-node in a SimpleXMLElement see the following example:
$xml = new SimpleXMLElement('<root><first><second><third/></second></first></root>');
$element = $xml->first->second->third;
$element[0] = "value";
This is pretty straight forward but you can see two things here:
The <third> element already exists in the document.
The code uses as simplexml-self-reference ([0]) which allows to set the XML value of the element variable (and not the variable). This is specific to how SimpleXMLElement works.
The second point also contains the solution to the problem how to deal with non-existent elements. $element[0] is NULL in case the element does not exists:
$xml = new SimpleXMLElement('<root><first><second/></first></root>');
$element = $xml->first->second->third;
var_dump($element[0]); # NULL
So let's try to conditionally add the third element in case it does not exists:
if ($xml->first->second->third[0] === NULL) {
$xml->first->second->third = "";
}
This does solve that problem. So the only thing left to do is to do that in an iterative fashion for all parts of the path:
first/second/third
To keep this easy, create a function for this:
/**
* Modify an elements value specified by a string-path.
*
* #param SimpleXMLElement $parent
* #param string $path
* #param string $value (optional)
*
* #return SimpleXMLElement the modified element-node
*/
function simplexml_deep_set(SimpleXMLElement $parent, $path, $value = '')
{
### <mocked> to be removed later: ###
if ($parent->first->second->third[0] === NULL) {
$parent->first->second->third = "";
}
$element = $parent->first->second->third;
### </mocked> ###
$element[0] = $value;
return $element;
}
Because the function is mocked, it can be used directly:
$xml = new SimpleXMLElement('<root><first><second/></first></root>');
simplexml_deep_set($xml, "first/second/third", "The XML Node Value");
$xml->asXML('php://output');
And this works:
<?xml version="1.0"?>
<root><first><second><third>The XML Node Value</third></second></first></root>
So now removing the mock. First insert the explode like you have it as well. Then all that needs to be done is to go along each step of the path and create the element conditionally if it yet does not exist. In the end $element will be the element to modify:
$steps = explode('/', $path);
$element = $parent;
foreach ($steps as $step)
{
if ($element->{$step}[0] === NULL) {
$element->$step = '';
}
$element = $element->$step;
}
This foreach is needed to replace the mock with a working version. Compare with the full function definition at a glance:
function simplexml_deep_set(SimpleXMLElement $parent, $path, $value = '')
{
$steps = explode('/', $path);
$element = $parent;
foreach ($steps as $step)
{
if ($element->{$step}[0] === NULL) {
$element->$step = "";
}
$element = $element->$step;
}
$element[0] = $value;
return $element;
}
Lets modify more crazy things to test it out:
$xml = new SimpleXMLElement('<root><first><second/></first></root>');
simplexml_deep_set($xml, "first/second/third", "The XML Node Value");
simplexml_deep_set(
$xml, "How/do/I/dynamically/create/a/php/simplexml/object/while/keeping/current/properties"
, "The other XML Node Value"
);
$xml->asXML('php://output');
Example-Output (beautified):
<?xml version="1.0"?>
<root>
<first>
<second>
<third>The XML Node Value</third>
</second>
</first>
<How>
<do>
<I>
<dynamically>
<create>
<a>
<php>
<simplexml>
<object>
<while>
<keeping>
<current>
<properties>The other XML Node Value</properties>
</current>
</keeping>
</while>
</object>
</simplexml>
</php>
</a>
</create>
</dynamically>
</I>
</do>
</How>
</root>
See it in action.
So I'm trying to write a function that does the following: I have about 20 or so XML files (someday I will have over a hundred) and in the header of each file is the name of a person who was a peer review editor <editor role="PeerReviewEditor">John Doe</editor>. I want to run through the directory where these files are stored and capture the name of the Peer-Review-Editor for that file. I want to end up with an variable $reviewEditorNames that contains all of the different names. (I will then use this to display a list of editors, etc.)
Here's what I've got so far. I'm worried about the last part. I feel like the attempt to turn $editorReviewName into $editorReviewNames is not going to combine the individuals for each file, but an array found within a given file (even if there is only one name in a given file, and thus it is an array of 1)
I'm grateful for your help.
function editorlist()
{
$filename = readDirectory('../editedtranscriptions');
foreach($filename as $file)
{
$xmldoc = simplexml_load_file("../editedtranscriptions/$file");
$xmldoc->registerXPathNamespace("tei", "http://www.tei-c.org/ns/1.0");
$reviewEditorName = $xmldoc->xpath("//tei:editor[#role='PeerReviewEditor']");
return $reviewEditorNames[] = $reviewEditorName;
}
}
I would put things more apart, that helps as well when you need to change your code later on.
Next to that, you need to check the return of the xpath, most likely you want to process only the first match (is there one editor per file?) and you want to return it as string.
If you put things into functions of it's own it's more easy to make a function to only do one thing and so it's easier to debug and improve things. E.g. you can first test if a editorFromFile function does what it should and then run it on multiple files:
/**
* get PeerReviewEditor from file
*
* #param string $file
* #return string
*/
function editorFromFile($file)
{
$xmldoc = simplexml_load_file($file);
$xmldoc->registerXPathNamespace("tei", "http://www.tei-c.org/ns/1.0");
$node = $xmldoc->xpath("//tei:editor[#role='PeerReviewEditor'][1]");
return (string) $node[0];
}
/**
* get editors from a path
*
* #param string $path
* #return array
*/
function editorlist($path)
{
$editors = array();
$files = glob(sprintf('%s/*.xml', $path), GLOB_NOSORT);
foreach($files as $file)
{
$editors[] = editorFromFile($file);
}
return $editors;
}
Just a little update:
function editorlist() {
$reviewEditorNames = array(); // init the array
$filename = readDirectory('../editedtranscriptions');
foreach($filename as $file) {
$xmldoc = simplexml_load_file("../editedtranscriptions/$file");
$xmldoc->registerXPathNamespace("tei", "http://www.tei-c.org/ns/1.0");
// add to the array
$result = $xmldoc->xpath("//tei:editor[#role='PeerReviewEditor']");
if (sizeof($result) > 0) {
$reviewEditorNames[] = (string)$result[0];
}
}
// return the array
return $reviewEditorNames;
}
I want to copy a record with all his relations.
I'm trying with:
$o = Doctrine::getTable('Table')->Find(x);
$copy = $object->copy();
$relations = $o->getRelations();
foreach ($relations as $name => $relation) {
$copy->$relation = $object->$relation->copy();
}
$copy->save();
This code doesn't works, but I think it's on the way.
I never could get the deep copy function to operate correctly.
I manually coded a deep copy function for one of my models like this
public function copyAndSave ()
{
$filters = array('id', 'created');
$survey = $this->copy();
$survey->Survey_Entries = new Doctrine_Collection("Survey_Model_Entry");
$survey->Assignment_Assignments = new Doctrine_Collection("Assignment_Model_Assignment");
$survey->Survey_Questions = new Doctrine_Collection("Survey_Model_Question");
$survey->save();
foreach ($this->Survey_Questions as $question)
{
$answers = $question->Survey_Answers;
$newQuestion = $question->copy();
$newQuestion->survey_surveys_id = $survey->id;
$newQuestion->save();
$newAnswers = new Doctrine_Collection("Survey_Model_Answer");
foreach($answers as $answer)
{
$answer = $answer->copy();
$answer->save();
$answer->survey_questions_id = $newQuestion->id;
$newAnswers->add($answer);
}
$newQuestion->Survey_Answers = $newAnswers;
$survey->Survey_Questions->add($newQuestion);
}
return $survey->save();
}
You can read about copy() here. It takes an optional parameter $deep:
$deep
whether to duplicates the objects targeted by the relations
So
$copy = $object->copy(true);
should do it.
Sorry if I'm resurrecting this thread...
I found myself in search of a solution recently where I needed to copy a record and retain the references of the original. A deep copy $record->copy(true) copies the references, which was no good for me. This was my solution:
$record = Doctrine_Core::getTable('Foo')->find(1);
$copy = $record->copy();
foreach($record->getTable()->getRelations() as $relation) {
if ($relation instanceof Doctrine_Relation_Association) {
$ids = array();
foreach ($relation->fetchRelatedFor($record) as $r) {
$ids[] = $r->getId();
}
$copy->link($relation->getAlias(), $ids);
}
}
if ($copy->isValid()) {
$copy->save();
}
Hope this helps :)
This is how i done, but some fix is needed.
$table = $entidade->getTable();
$relations = $table->getRelations();
foreach($relations as $relation => $data) {
try {
$entity->loadReference($relation);
} catch(Exception $e) {
die($e->getMessage());
}
}
I am using Symfony1.4.1 and that uses Doctrine 1.2.1 (I think).
I have been trying to make a function that did all the above myself, when I found one that already exists.
Try this in any function and look at the results:
$tmp=$this->toArray(TRUE);
var_dump($tmp);
$this->refreshRelated();
$tmp=$this->toArray();
var_dump($tmp);
$tmp=$this->toArray(TRUE);
var_dump($tmp);
exit();
I am going to try two different things:
A/ put $this->refreshRelated() into the constructor of all my model objects.
B/ write a function that takes an array depicting the object graph that I want populated. Calling the function refereshRelatedGraph($objectGraphArray). With the right structure of the array (having all the appropriate relation names at each level), I could control which relations get populated and which don't. One use for this is to populate only children, not parent relations. The other is for when a ERD/Schema/ObjectGraph has an element that is 'owned' by more than one object (many to many, other special circumstances that I have), I could control which side of the relationships get pre(non lazy) loaded.