Convert multidimensional array to dot notation in Symfony2 - php

Symfony converts nested YAML and PHP array translation files to a dot notation, like this: modules.module.title.
I'm writing some code that exports YAML translation files to a database, and I need to flatten the parsed files to a dot notation.
Does anyone know which function Symfony uses to flatten nested arrays to dot notation?
I cannot find it anywhere in the source code.

It's the flatten() method in Symfony\Component\Translation\Loader\ArrayLoader:
<?php
/**
* Flattens an nested array of translations.
*
* The scheme used is:
* 'key' => array('key2' => array('key3' => 'value'))
* Becomes:
* 'key.key2.key3' => 'value'
*
* This function takes an array by reference and will modify it
*
* #param array &$messages The array that will be flattened
* #param array $subnode Current subnode being parsed, used internally for recursive calls
* #param string $path Current path being parsed, used internally for recursive calls
*/
private function flatten(array &$messages, array $subnode = null, $path = null)
{
if (null === $subnode) {
$subnode = &$messages;
}
foreach ($subnode as $key => $value) {
if (is_array($value)) {
$nodePath = $path ? $path.'.'.$key : $key;
$this->flatten($messages, $value, $nodePath);
if (null === $path) {
unset($messages[$key]);
}
} elseif (null !== $path) {
$messages[$path.'.'.$key] = $value;
}
}
}

I don't know how what is written in previous Symfony versions, but in Symfony 4.2 onwards translations are returned already flattened.
Example controller which returns the messages catalogue translations. In my case I used this response to feed the i18next js library.
<?php
declare(strict_types=1);
namespace Conferences\Controller;
use Symfony\Component\HttpFoundation\JsonResponse;
use Symfony\Component\HttpKernel\Exception\ServiceUnavailableHttpException;
use Symfony\Component\Translation\TranslatorBagInterface;
use Symfony\Contracts\Translation\TranslatorInterface;
final class TranslationsController
{
public function __invoke(TranslatorInterface $translator): JsonResponse
{
if (!$translator instanceof TranslatorBagInterface) {
throw new ServiceUnavailableHttpException();
}
return new JsonResponse($translator->getCatalogue()->all()['messages']);
}
}
Route definition:
translations:
path: /{_locale}/translations
controller: App\Controller\TranslationsController
requirements: {_locale: pl|en}

Related

PHP - iterate through different value types

I have object of class $values like:
Array
(
[0] => App\ValueObject\Features Object
(
[feature:App\ValueObject\Features:private] => CONNECT_NETWORKS_ON_SIGN_UP
[value:App\ValueObject\Features:private] => 1
)
[1] => App\ValueObject\Features Object
(
[feature:App\ValueObject\Features:private] => SHOW_BILLING
[value:App\ValueObject\Features:private] => 1
)
[2] => App\ValueObject\Features Object
(
[feature:App\ValueObject\Features:private] => FEATURE_FLAGS
[value:App\ValueObject\Features:private] => 'activity'
)
)
All array keys are returning boolean type value expect one, which returns string value.
My result with the code:
$arrays = array_map(
function($value) { return [strtolower((string) $value->getFeature())]; },
iterator_to_array($values)
);
return array_merge(...$arrays);
returns list of feature names like:
"features": [
"connect_networks_on_sign_up",
"show_billing",
"feature_flags"
]
What I want to edit is that for the last one we write its value NOT feature name ($value->getValue())
I am assuming that using in_array() PHP function would be the best approach here but I can't find a way to use it within my current method.
Tried with foreach() loop but nothing happens, like it's something wrong:
$features = [];
foreach ($values as $value)
{
$setParam = $value->getFeature();
if ($value == 'FEATURE_FLAGS') {
$setParam = $value->getValue();
}
$features[] = strtolower((string) $setParam);
}
return $features;
Can someone help?
Thanks
You should probably operate on the feature code FEATURE_FLAGS, rather than assuming that the last feature in the array always contains the flags. Using your existing code, that could be as simple as:
$arrays = array_map(
function($value)
{
/*
* If the current Features object has the feature code FEATURE_FLAGS,
* return the value itself, otherwise return the feature code in lowercase
*/
return ($value->getFeature() == 'FEATURE_FLAGS') ? [$value->getValue()]:[strtolower((string) $value->getFeature())];
},
iterator_to_array($values)
);
If you want to define an array of feature codes that you need to treat this way, you can define it internally in the callback, but it is probably a better idea to define it externally. You can then pass it into the callback with use
/*
* Define an array of feature codes that we want to return
* values for
*/
$valueCaptureFeatures = ['FEATURE_FLAGS'];
$arrays = array_map(
function($value) use ($valueCaptureFeatures) // <-- Put our $valueCaptureFeatures in the scope of the callback
{
/*
* If the current Features object has a feature code in the $valueCaptureFeatures array,
* return the value itself, otherwise return the feature code in lowercase
*/
return (in_array($value->getFeature(), $valueCaptureFeatures)) ? [$value->getValue()]:[strtolower((string) $value->getFeature())];
},
iterator_to_array($values)
);
Working example:
// Mock the Features class
class Features
{
private $feature;
private $value;
public function __construct($feature, $value)
{
$this->feature = $feature;
$this->value = $value;
}
public function getFeature()
{
return $this->feature;
}
public function setFeature($feature): void
{
$this->feature = $feature;
}
public function getValue()
{
return $this->value;
}
public function setValue($value): void
{
$this->value = $value;
}
}
// Mock an iterator with test Feature instances
$values = new ArrayIterator( [
new Features('CONNECT_NETWORKS_ON_SIGN_UP', 1),
new Features('SHOW_BILLING', 1),
new Features('FEATURE_FLAGS', 'activity')
]);
/*
* Define an array of feature codes that we want to return
* values for
*/
$valueCaptureFeatures = ['FEATURE_FLAGS'];
$arrays = array_map(
function($value) use ($valueCaptureFeatures) // <-- Put our $valueCaptureFeatures in the scope of the callback
{
/*
* If the current Features object has a feature code in the $valueCaptureFeatures array,
* return the value itself, otherwise return the feature code in lowercase
*/
return (in_array($value->getFeature(), $valueCaptureFeatures)) ? [$value->getValue()]:[strtolower((string) $value->getFeature())];
},
iterator_to_array($values)
);
$output = array_merge(...$arrays);
$expectedResult = [
'connect_networks_on_sign_up',
'show_billing',
'activity'
];
assert($output == $expectedResult, 'Result should match expectations');
print_r($output);

How can I make big multidimensional variables seem shorter in PHP?

I'm creating a php class which is getting slightly out of hand the deeper it gets.
Here's an example:
unset($this->file[$key]->inspect->formats);
unset($this->file[$key]->inspect->tags);
unset($this->file[$key]->inspect->chapters);
unset($this->file[$key]->inspect->annotations);
unset($this->file[$key]->inspect->automatic_captions);
unset($this->file[$key]->inspect->subtitles);
$this->file[$key]->inspect->name = trim($this->file[$key]->inspect->name);
$this->file[$key]->inspect->artist = trim($this->file[$key]->inspect->artist);
Instead of writing $this->file[$key]->inspect for every single variable I want to use is there a way I can set a variable e.g $inspect to take this place?
So that when I write $inspect->subtitles it'll know what I really mean and affect the main $this->file[$key]->inspect->subtitles?
$inspect = &$this->file[$key]->inspect;
declare this. Now you can set your data like this
$inspect->formats = 'format';
$inspect->subs = 'subs';
// ...
adding the & you will affect the variable and not only a copy of this variable
here are explanations about references http://php.net/manual/en/language.references.whatare.php
One approach you could use is replicate the object_get() helper method from Laravel that will fetch elements based on dot notation.
/**
* Get an item from an object using "dot" notation.
*
* #param object $object
* #param string $key
* #param mixed $default
* #return mixed
*/
function object_get($object, $key, $default = null)
{
if (is_null($key) || trim($key) == '') return $object;
foreach (explode('.', $key) as $segment)
{
if ( ! is_object($object) || ! isset($object->{$segment}))
{
return value($default);
}
$object = $object->{$segment};
}
return $object;
}
$ob = new StdClass();
$ob->property->name->value = 'Lol';
echo object_get($ob, 'property.name.value');
Unfortunately there'd be a bit of extra implementation if the $object->property was an array like in your example.

Unit test: using the proper terminology for mocking/stubbing

After fundamental changes on my project system architecture, I find myself in a situation where I would need to create "fake" implementation in order to test some functionality that used to be public like the following:
/**
* Display the template linked to the page.
*
* #param $newSmarty Smarty object to use to display the template.
*
* #param $parameters associative Array containing the values to pass to the template.
* The key is the name of the variable in the template and the value is the value of the variable.
*
* #param $account child class in the AccountManager hierarchy
*
* #param $partialview String name of the partial view we are working on
*/
protected function displayPageTemplateSmarty(Smarty &$newSmarty, array $parameters = array(), AccountManager $account = NULL, string $partialview = "")
{
$this->smarty = $newSmarty;
if (is_file(
realpath(dirname(__FILE__)) . "/../../" .
Session::getInstance()->getCurrentDomain() . "/view/" . (
!empty($partialview) ?
"partial_view/" . $partialview :
str_replace(
array(".html", "/"),
array(".tpl", ""),
Session::getInstance()->getActivePage()
)
)
)) {
$this->smarty->assign(
'activeLanguage',
Session::getInstance()->getActiveLanguage()
);
$this->smarty->assign('domain', Session::getInstance()->getCurrentDomain());
$this->smarty->assign(
'languages',
Languagecontroller::$supportedLanguages
);
$this->smarty->assign(
'title',
Languagecontroller::getFieldTranslation('PAGE_TITLE', '')
);
$this->smarty->assign_by_ref('PageController', $this);
$htmlTagBuilder = HTMLTagBuilder::getInstance();
$languageController = LanguageController::getInstance();
$this->smarty->assign_by_ref('htmlTagBuilder', $htmlTagBuilder);
$this->smarty->assign_by_ref('languageController', $languageController);
if (!is_null($account)) {
$this->smarty->assign_by_ref('userAccount', $account);
}
if (!is_null($this->menuGenerator)) {
$this->smarty->assign_by_ref('menuGenerator', $this->menuGenerator);
}
foreach ($parameters as $key => $value) {
$this->smarty->assign($key, $value);
}
$this->smarty->display((!empty($partialview) ?
"partial_view/" . $partialview :
str_replace(
array(".html", "/"),
array(".tpl", ""),
Session::getInstance()->getActivePage()
)
));
}
}
In this case, the PageController class used to be called directly in controllers, but is now an abstract class extended by the controllers and my unit tests can no longer access the method.
I also have methods like this one in my new session wrapper class that can only be used in very specific context and for which I really need to create fake page implementation to test them.
/**
* Add or update an entry to the page session array.
*
* Note: can only be updated by the PageController.
*
* #param $key String Key in the session array.
* Will not be added if the key is not a string.
*
* #param $value The value to be added to the session array.
*
* #return Boolean
*/
public function updatePageSession(string $key, $value)
{
$trace = debug_backtrace();
$updated = false;
if (isset($trace[1]) and
isset($trace[1]['class']) and
$trace[1]['class'] === 'PageController'
) {
$this->pageSession[$key] = $value;
$updated = true;
}
return $updated;
}
Even though I read a few article, it is still quite unclear in my mind if those fake classes should be considered as "stub" or a "mock" (or even "fake", "dummy" and so on).
I really need to use the proper terminology since my boss is expecting me (in a close future) to delegate most of my workload with oversea developers.
How would you call those fake class implementation created solely for testing purpose in order to be self-explanatory?
Gerard Meszaros explains the terminology of dummies, stubs, spies, mocks, and fakes here.
You can find examples from the PHP world here.

Unserialize() offset error when using CodeIgniter 2.2 Sessions with Objects

I'm trying to debug some old code from CodeIgniter 2.2. When running some data thru Session, I noticed an unserialize error, Message: unserialize(): Error at offset 160 of 163 bytes. After doing some debugging and research, I found out it's a common backslash issue when unserializing data from Sessions.
The serialized data I'm using has objects of data with backslashes in them, which causes the errors to occur. I'm in need of a replacement that can handle standard class objects as well.
Could someone recommend a quick replacement for codeigniter's Session _serialize() and _unserialize() methods?
public function data_test() {
$input = array(
(object)array('name' => 'test2', 'desc' => 'bla bla ob/gyn'),
(object)array('name' => 'test2', 'desc' => 'bla bla ob\\gyn'),
);
var_dump($input);
$data = $this->_serialize($input);
var_dump($data);
$result = $this->_unserialize($data);
var_dump($result);
}
// --------------------------------------------------------------------
/**
* Serialize an array
*
* This function first converts any slashes found in the array to a temporary
* marker, so when it gets unserialized the slashes will be preserved
*
* #access private
* #param array
* #return string
*/
function _serialize($data) {
if (is_array($data)) {
foreach ($data as $key => $val) {
if (is_string($val)) {
$data[$key] = str_replace('\\', '{{slash}}', $val);
}
}
} else {
if (is_string($data)) {
$data = str_replace('\\', '{{slash}}', $data);
}
}
return serialize($data);
}
// --------------------------------------------------------------------
/**
* Unserialize
*
* This function unserializes a data string, then converts any
* temporary slash markers back to actual slashes
*
* #access private
* #param array
* #return string
*/
function _unserialize($data) {
$data = unserialize(strip_slashes($data));
if (is_array($data)) {
foreach ($data as $key => $val) {
if (is_string($val)) {
$data[$key] = str_replace('{{slash}}', '\\', $val);
}
}
return $data;
}
return (is_string($data)) ? str_replace('{{slash}}', '\\', $data) : $data;
}
/**
* Serialize an array
*
* This function serializes the data and then base64_encodes it for
* storage with memcached. This avoids the common backslash issue.
*
* #access private
* #param array
* #return string
*/
function _serialize($data) {
return base64_encode(serialize($data));
}
// --------------------------------------------------------------------
/**
* Unserialize
*
* This function unserializes a data string. I first base64_decodes
* the data from memcached storage.
*/
function _unserialize($data) {
return unserialize(base64_decode($data));
}
You can sometimes come across this issue if you are using different versions of PHP, or if you change the version of PHP you are using while a session was open.
For example if you have a session cookie with an app that uses PHP 5.6.* and then you try to use it with an app (that resides on another sub-domain) that uses PHP 7.2.*, then you are going to get a warning error. Or, if you had an open session and then you changed the version of PHP that you are using with your app (say if you are developing locally and switching around PHP versions), then you'll get the warning. So best to use serialize/unserialize and with PHP version that does not change.

Convert associative array to XML in PHP

I was wondering if a function capable of converting an associative array to an XML document exists in PHP (or some widely available PHP library).
I've searched quite a lot and could only find functions that do not output valid XML. I believe that the array I'm testing them on is correctly constructed, since it can be correctly used to generate a JSON document using json_encode. However, it is rather large and it is nested on four levels, which might explain why the functions I've tried so far fail.
Ultimately, I will write the code to generate the XML myself but surely there must be a faster way of doing this.
I realize I am a Johnny-Come-Lately here, but I was working with the VERY same problem -- and the tutorials I found out there would almost (but not quite upon unit testing) cover it.
After much frustration and research, here is what I cam up with
XML To Assoc. Array:
From http://www.php.net/manual/en/simplexml.examples-basic.php
json_decode( json_encode( simplexml_load_string( $string ) ), TRUE );
Assoc. Array to XML
notes:
XML attributes are not handled
Will also handle nested arrays with numeric indices (which are not valid XML!)
From http://www.devexp.eu/2009/04/11/php-domdocument-convert-array-to-xml/
/// Converts an array to XML
/// - http://www.devexp.eu/2009/04/11/php-domdocument-convert-array-to-xml/
/// #param <array> $array The associative array you want to convert; nested numeric indices are OK!
function getXml( array $array ) {
$array2XmlConverter = new XmlDomConstructor('1.0', 'utf-8');
$array2XmlConverter->xmlStandalone = TRUE;
$array2XmlConverter->formatOutput = TRUE;
try {
$array2XmlConverter->fromMixed( $array );
$array2XmlConverter->normalizeDocument ();
$xml = $array2XmlConverter->saveXML();
// echo "\n\n-----vvv start returned xml vvv-----\n";
// print_r( $xml );
// echo "\n------^^^ end returned xml ^^^----\n"
return $xml;
}
catch( Exception $ex ) {
// echo "\n\n-----vvv Rut-roh Raggy! vvv-----\n";
// print_r( $ex->getCode() ); echo "\n";
// print_r( $->getMessage() );
// var_dump( $ex );
// echo "\n------^^^ end Rut-roh Raggy! ^^^----\n"
return $ex;
}
}
... and here is the class to use for the $array2XmlConverter object:
/**
* Extends the DOMDocument to implement personal (utility) methods.
* - From: http://www.devexp.eu/2009/04/11/php-domdocument-convert-array-to-xml/
* - `parent::` See http://www.php.net/manual/en/class.domdocument.php
*
* #throws DOMException http://www.php.net/manual/en/class.domexception.php
*
* #author Toni Van de Voorde
*/
class XmlDomConstructor extends DOMDocument {
/**
* Constructs elements and texts from an array or string.
* The array can contain an element's name in the index part
* and an element's text in the value part.
*
* It can also creates an xml with the same element tagName on the same
* level.
*
* ex:
\verbatim
<nodes>
<node>text</node>
<node>
<field>hello</field>
<field>world</field>
</node>
</nodes>
\verbatim
*
*
* Array should then look like:
\verbatim
array(
"nodes" => array(
"node" => array(
0 => "text",
1 => array(
"field" => array (
0 => "hello",
1 => "world",
),
),
),
),
);
\endverbatim
*
* #param mixed $mixed An array or string.
*
* #param DOMElement[optional] $domElement Then element
* from where the array will be construct to.
*
*/
public function fromMixed($mixed, DOMElement $domElement = null) {
$domElement = is_null($domElement) ? $this : $domElement;
if (is_array($mixed)) {
foreach( $mixed as $index => $mixedElement ) {
if ( is_int($index) ) {
if ( $index == 0 ) {
$node = $domElement;
}
else {
$node = $this->createElement($domElement->tagName);
$domElement->parentNode->appendChild($node);
}
}
else {
$node = $this->createElement($index);
$domElement->appendChild($node);
}
$this->fromMixed($mixedElement, $node);
}
}
else {
$domElement->appendChild($this->createTextNode($mixed));
}
}
} // end of class
No. At least there is no such in-built function. It's not a probrem to write it at all.
surely there must be a faster way of doing this
How do you represent attribute in array? I can assume keys are tags and values are this tags content.
Basic PHP Array -> JSON works just fine, cause those structure is... well... almost the same.
Call
// $data = array(...);
$dataTransformator = new DataTransformator();
$domDocument = $dataTransformator->data2domDocument($data);
$xml = $domDocument->saveXML();
DataTransformator
class DataTransformator {
/**
* Converts the $data to a \DOMDocument.
* #param array $data
* #param string $rootElementName
* #param string $defaultElementName
* #see MyNamespace\Dom\DataTransformator#data2domNode(...)
* #return Ambigous <DOMDocument>
*/
public function data2domDocument(array $data, $rootElementName = 'data', $defaultElementName = 'item') {
return $this->data2domNode($data, $rootElementName, null, $defaultElementName);
}
/**
* Converts the $data to a \DOMNode.
* If the $elementContent is a string,
* a DOMNode with a nested shallow DOMElement
* will be (created if the argument $node is null and) returned.
* If the $elementContent is an array,
* the function will applied on every its element recursively and
* a DOMNode with a nested DOMElements
* will be (created if the argument $node is null and) returned.
* The end result is always a DOMDocument object.
* The casue is, that a \DOMElement object
* "is read only. It may be appended to a document,
* but additional nodes may not be appended to this node
* until the node is associated with a document."
* See {#link http://php.net/manual/en/domelement.construct.php here}).
*
* #param Ambigous <string, mixed> $elementName Used as element tagname. If it's not a string $defaultElementName is used instead.
* #param Ambigous <string, array> $elementContent
* #param Ambigous <\DOMDocument, NULL, \DOMElement> $parentNode The parent node is
* either a \DOMDocument (by the method calls from outside of the method)
* or a \DOMElement or NULL (by the calls from inside).
* Once again: For the calls from outside of the method the argument MUST be either a \DOMDocument object or NULL.
* #param string $defaultElementName If the key of the array element is a string, it determines the DOM element name / tagname.
* For numeric indexes the $defaultElementName is used.
* #return \DOMDocument
*/
protected function data2domNode($elementContent, $elementName, \DOMNode $parentNode = null, $defaultElementName = 'item') {
$parentNode = is_null($parentNode) ? new \DOMDocument('1.0', 'utf-8') : $parentNode;
$name = is_string($elementName) ? $elementName : $defaultElementName;
if (!is_array($elementContent)) {
$content = htmlspecialchars($elementContent);
$element = new \DOMElement($name, $content);
$parentNode->appendChild($element);
} else {
$element = new \DOMElement($name);
$parentNode->appendChild($element);
foreach ($elementContent as $key => $value) {
$elementChild = $this->data2domNode($value, $key, $element);
$parentNode->appendChild($elementChild);
}
}
return $parentNode;
}
}
PHP's DOMDocument objects are probably what you are looking for. Here is a link to an example use of this class to convert a multi-dimensional array into an xml file - http://www.php.net/manual/en/book.dom.php#78941
function combArrToXML($arrC=array(), $root="root", $element="element"){
$doc = new DOMDocument();
$doc->formatOutput = true;
$r = $doc->createElement( $root );
$doc->appendChild( $r );
$b = $doc->createElement( $element );
foreach( $arrC as $key => $val)
{
$$key = $doc->createElement( $key );
$$key->appendChild(
$doc->createTextNode( $val )
);
$b->appendChild( $$key );
$r->appendChild( $b );
}
return $doc->saveXML();
}
Example:
$b=array("testa"=>"testb", "testc"=>"testd");
combArrToXML($b, "root", "element");
Output:
<?xml version="1.0"?>
<root>
<element>
<testa>testb</testa>
<testc>testd</testc>
</element>
</root>
surely there must be a faster way of doing this
If you've got PEAR installed, there is. Take a look at XML_Seralizer. It's beta, so you'll have to use
pear install XML_Serializer-beta
to install
I needed a solution which is able to convert arrays with non-associative subarrays and content which needs to be escaped with CDATA (<>&). Since I could not find any appropriate solution, I implemented my own based on SimpleXML which should be quite fast.
https://github.com/traeger/SimplestXML (this solution supports an (Associative) Array => XML and XML => (Associative) Array conversion without attribute support). I hope this helps someone.

Categories