Does anyone have any clue as to how I can add an attribute to a SoapVar object? It seems like it would be simple, but I can't get it to take/work.
I've looked at the PHP docs and at the following stackoverflow question:
Documentation on SoapVar,
stackoverflow question:
SoapVar/Param and nested, repeated elements in SOAP
I'm trying to add an attribute like this array example, but using complex SoapVar objects instead.
<?php
$amount['_'] = 25;
$amount['currencyId'] = 'GBP';
$encodded = new SoapVar($amount, SOAP_ENC_OBJECT);
?>
and end result wound be
<amount currencyId="GBP">25</amount>
Thanks.
Getting attributes into SOAP elements is a bit of a hassle. The way they implemented it is a bit confusing.
First thing to do is add the attributes to the wsdl file that SoapServer uses to correctly read and respond to the SOAP requests.
<xs:complexType name="encryptionContext">
<xs:simpleContent>
<xs:extension base="xs:string">
**<xs:attribute name="type" type="tns:encryptionType" />**
</xs:extension>
</xs:simpleContent>
</xs:complexType>
We will have to tell SoapServer to use a php helper class by passing it in the options as classmap:
$soap_server = new \SoapServer($wsdl_file, array(
'cache_wsdl' => 1,
'trace' => true,
'classmap' => array('mediaCollection' => 'SoapMediaHelper')
));
What we are mapping here is the SOAP element name mediaCollection to one of our classes, SoapMediaHelper. Instead of returning arrays, we can now return a class, in this case, it's named SoapMediaHelper. The class can have soap-element=>value pairs as well as soap-attribute=>value pairs.
Assuming we already have made a class that handles mediaCollections, this tells SoapServer to map a class called SoapMediaHelper to it. The class is really simple:
class SoapMediaHelper
{
public function __construct(Array $properties = array())
{
foreach ($properties as $key => $value) {
$this->{$key} = $value;
}
}
}
The properties of this class have to be populated. These properties should be both the tagname=>value pairs as well as the attribute name and value pair(s) for the attributes we want to add to our mediaCollection. SoapServer will figure out which is which according to our wsdl file.
We will still have to populate this object, which we can do with yet another class.
class SoapVarHelper
{
public function get($the_playlist, $playlist_id, $owned_by_user){
/* The SoapMediaHelper class is mapped to the mediaCollection wsdl.
* This is only needed to be able to set attributes on the XML nodes while using php's SoapServer
* */
$media_helper = new SoapMediaHelper($the_playlist);
/* For this type, the following xml attributes have to be set. (Not in the wsdl example above.) */
if($playlist_id === 'playlists'){
$media_helper->readOnly = false;
$media_helper->userContent = true;
$media_helper->renameable = false;
$media_helper->canDeleteItems = true;
}
if($owned_by_user){
$media_helper->readOnly = false;
$media_helper->userContent = false;
$media_helper->renameable = true;
$media_helper->canDeleteItems = true;
$media_helper->canReorderItems = true;
}
return new \SoapVar($media_helper, SOAP_ENC_OBJECT);
}
}
This class should be called with the normal tagname=>value pairs. It then adds the attributes we want. In this case conditionally. We feed our SoapMediaHelper object to SoapVar. (We told SoapServer earlier that this is fine.)
Now the last thing we need to do is, in our mediaCollection class, to use the helper SoapVarHelper to return a SoapMediaHelper (the one we told SoapServer about earlier).
In our mediaCollection we have a function get_metadata_for_root:
public function get_metadata_for_root($user_id, $index, $count){
$titles = array(
'slides' => 'Featured',
);
$media_metadata = array();
foreach($titles as $key => $title){
$playlist = array(
'id' => $key,
'title' => $title,
'img' => $this->_utils->get_url() . '/public/sonos/images/browse-icons/icon-default-legacy.png'
);
**$media_metadata[] = $this->_soap_var_helper->get($playlist, $key, false);**
}
$res = array(
'count' => count($media_metadata),
'index' => 0,
'total' => count($media_metadata),
'mediaCollection' => $media_metadata
);
}
For every mediaCollection result we pass it through the soap_var_helper to make sure not only the element=>value pairs are added, but also the attributes that we want on it.
TO SUMMARIZE:
Make sure you feed the SoapServer with a wsdl file, so it know the elements and the attributes.
In the SoapServer options add classmap in order to tell SoapServer that it is fine when we feed it a SoapMediaHelper object instead of the regular input.
Before responding to a request for, in this case, mediaCollection, pass this response through the SoapMediaHelper. The SoapVarHelper will map all the properties=>value pairs as class properties, then the SoapMediaHelper will add attributes (also as name=>value pairs) to it.
SoapServer will take care of the rest.
Related
I try to implement a webservice Client using php and got stuck...
I'm using an existing webservice called metadataservice with a known wsdl.
I'll use wsdl2phpgenerator to create the php classes for the datatypes and the Service itself.
Using one of the Webservice Methods (addMetadataToObject), I have to send an Array of objects to the Server.
There is a base class:
class AssetInfo
{
public $dataFieldId = null;
public $dataFieldName = null;
public $dataFieldTagName = null;
public function __construct($dataFieldId, $dataFieldName, $dataFieldTagName)
{
$this->dataFieldId = $dataFieldId;
$this->dataFieldName = $dataFieldName;
$this->dataFieldTagName = $dataFieldTagName;
}
}
and a derived class Holding string values (there are also other derived classes for Longs etc.):
class StringAssetInfo extends AssetInfo
{
public $value = null;
public function __construct($dataFieldId, $dataFieldName,$dataFieldTagName, $value)
{
parent::__construct($dataFieldId, $dataFieldName, $dataFieldTagName);
$this->value = $value;
}
}
For the call of Metadataservice->addMetadataToObject there is also a addMetadataToObject defined:
class addMetadataToObject
{
public $objectId = null;
public $objectType = null;
public $assetInfos = null;
public function __construct($objectId, $objectType)
{
$this->objectId = $objectId;
$this->objectType = $objectType;
}
}
The property $assetInfos should hold an Array of AssetInfo objects. wdsl2phpgenerator creates a class for my MetadataService which is derived from SoapClient. This class provides all the avialable Methods for this Service. Here I only show the addMetadataToObject Method:
public function addMetadataToObject(addMetadataToObject $parameters)
{
return $this->__soapCall('addMetadataToObject', array($parameters));
}
My Code does:
// Define the Data
$ServiceOptions = [];
$AssetInfos = [];
$AssetInfo = new StringAssetInfo(2, "TitleName", "TitleName","New Title Name);
array_push($AssetInfos, $AssetInfo);
// Create the Service
$Service = new MetadataService($ServiceOptions, getServiceWSDL($Options, "MetadataService"));
$Service->__setSoapHeaders(getGalaxySoapHeader($Options));
$NewMetaData = new addMetadataToObject(61755, "ASSET");
$NewMetaData->assetInfos = $AssetInfos;
// Call the Service
$failedAssets = $Service->addMetadataToObject($NewMetaData);
The call throws a Soap Exception that a value could not be extracted. Which makes me wonder. I started to monitor the traffic to the Soap Server using wireshark and yes....there is no value anymore as defined in the StringAsset Info Class...Here is the Soap Body shown by wireshark:
<SOAP-ENV:Body>
<ns1:addMetadataToObject>
<objectId>61755</objectId>
<objectType>ASSET</objectType>
<assetInfos>
<dataFieldId>2</dataFieldId>
<dataFieldName>TitleName</dataFieldName>
<dataFieldTagName>TitleName</dataFieldTagName>
</assetInfos>
</ns1:addMetadataToObject>
Id</SOAP-ENV:Body>
I would expect a tag New Title Name. But ist gone. When I checked the $NewMetaData object in my Code or the $Parameter object in $Service->addMetadataToObject I can see that the property "Value" is defined and set.
For me it seems, that the call to
return $this->__soapCall('addMetadataToObject', array($parameters));
only accepts the properties of the base class AssetInfo but not the properties from the derived class StringAssetInfo.
I also changed the Code to use an Array (instead of an object) for $AssetInfo:
$AssetInfo = array("dataFieldId"=>2, "dataFieldName"=>"TitleName","dataFieldTagName"=>"TitleName, "value"=>"New Title Name");
But without any change. It seems that we have here some Kind of runtime type conversion or type alignment but I can't see the reason of this. I'm still new to webservices at all and also on php (however I have to use both for the Moment:-)
Can anybody comment or give me a hint what's happening here?
I was able to realize it by using Arrays and soapvars, Please note my comments in the code:
$ServiceOptions = [];
$AssetInfos = [];
// I have to use an Array because the Server depends on the order of the properties. I wasn't able to define expected order using the existing objects but with arrays
$AssetInfo = array("dataFieldId"=>2, "dataFieldName"=>"TitleName","dataFieldTagName"=>"TitleName, "value"=>"New Title Name");
// instead of pushing the Array directly, I create an instance of an SoapVar, pass the Array as data and set the Encoding, the expected type and the Namespace uri
array_push($AssetInfos, new SoapVar($AssetInfo, SOAP_ENC_OBJECT, "StringAssetInfo", "http://metadataservice.services.provider.com"));
array_push($AssetInfos, $AssetInfo);
// Create the Service
$Service = new MetadataService($ServiceOptions, getServiceWSDL($Options, "MetadataService"));
$Service->__setSoapHeaders(getGalaxySoapHeader($Options));
$NewMetaData = new addMetadataToObject(61755, "ASSET");
$NewMetaData->assetInfos = $AssetInfos;
// Call the Service
$failedAssets = $Service->addMetadataToObject($NewMetaData);
This produced the expected Output in the Soap Body (and also added some namespaces to the Soap envelope
<SOAP-ENV:Body>
<ns1:addMetadataToObject>
<objectId>61755</objectId>
<objectType>ASSET</objectType>
<assetInfos xsi:type="ns1:StringAssetInfo">
<dataFieldId>2</dataFieldId>
<dataFieldName>TitleName</dataFieldName>
<dataFieldTagName>TitleName</dataFieldTagName>
<value>New Titel Name 1146</value>
</assetInfos>
</ns1:addMetadataToObject>
</SOAP-ENV:Body>
Here's how I'm setting up my params for the Soap call:
$params = array(
"connectionToken" => $this->token,
"inboxName" => $this->inboxName
);
$wrapper = new \stdClass();
$typedVar = new \SoapVar($value, XSD_STRING, "string", "http://www.w3.org/2001/XMLSchema");
$wrapper->anyType = $typedVar;
$params["fnParameterValues"] = $wrapper;
This creates the correct XML structure for the request except that if $value = null then I need to add an attribute to the anyType node of nil="true" (actually more accurately:- xsi:nil="true"). How can I achieve this?
I know it is a bit late, but I had the same problem and the solution I found could be helpful for others.
If you are using WSDL, the built-in PHP SoapServer should add this attribute automatically to any element with null value that is defined as nillable, to accommodate the element to the WSDL definition.
If you need to add an attribute manually, the only way to do that seems to be capturing the output and modifying the XML response.
You can use the Zend SoapServer wrapper (https://docs.zendframework.com/zend-soap/server/) and post-process the response (https://docs.zendframework.com/zend-soap/server/#response-post-processing), like this:
// Get a response as a return value of handle(),
// instead of emitting it to standard output:
$server->setReturnResponse(true);
$response = $server->handle();
if ($response instanceof SoapFault) {
// Manage exception
/* ... */
} else {
// Post-process response and return it
// I. e., load response as XML document, add attribute to node...
/* ... */
}
What I'm using:
I'm using the JMSSerializerBundle to deserialize my JSON object from a POST request.
Description of the problem:
One of the vaules in my JSON is an Id. I'd like to replace this Id with the correct object before the deserialization occurs.
Unfortunately, JMSSerializerBundle does not have a #preDeserializer annotation.
The problem I'm facing (and that I would have faced if there was an #preDeserializer annotation anyway) is that I would like to create a generic function for all my entities.
Question:
How do I replace my Id with the corresponding object in the most generic way possible ?
You also do your own hydratation as I did (with Doctrine):
Solution
The IHydratingEntity is an interface which all my entities implement.
The hydrate function is used in my BaseService generically. Parameters are the entity and the json object.
At each iteration, the function will test if the method exists then it will call the reflection function to check if the parameter's method (setter) also implements IHydratingEntity.
If it's the case, I use the id to get the entity from the database with Doctrine ORM.
I think it's possible to optimize this process, so please be sure to share your thoughts !
protected function hydrate(IHydratingEntity $entity, array $infos)
{
#->Verification
if (!$entity) exit;
#->Processing
foreach ($infos as $clef => $donnee)
{
$methode = 'set'.ucfirst($clef);
if (method_exists($entity, $methode))
{
$donnee = $this->reflection($entity, $methode, $donnee);
$entity->$methode($donnee);
}
}
}
public function reflection(IHydratingEntity $entity, $method, $donnee)
{
#->Variable declaration
$reflectionClass = new \ReflectionClass($entity);
#->Verification
$idData = intval($donnee);
#->Processing
foreach($reflectionClass->getMethod($method)->getParameters() as $param)
{
if ($param->getClass() != null)
{
if ($param->getClass()->implementsInterface(IEntity::class))
#->Return
return $this->getDoctrine()->getRepository($param->getClass()->name)->find($idData);
}
}
#->Return
return $donnee;
}
I'm trying to serialize an XML document containing entities to insert into Doctrine MySQL database.
I got, for example, these two attributes in my entity :
$companyId
$companyName
The problem is that instead of something like this into my XML doc :
<company>
<id>8888</id>
<name>MyCompany</name>
</company>
I got something like this :
<company id="8888" name="MyCompany"/>
The XML is generated by an independant company I work with ; so I can't change it.
So the Symfony2 serializer is creating an empty $company attribute :(
Is there a simple way to costumize the seralizing process like I want ? Or do I have to implement a complete independant method ?
Thanks a lot.
I'd create a simple Denormalizer because attributes are already parsed by default XmlEncoder. It adds a special character # in at the beggining of the key.
Without tweaking alot you could add a context parameter like use_attributes which your custom denormalizer can understand. Here's an example
use Symfony\Component\Serializer\Normalizer\DenormalizerInterface;
class AttributesDenormalizer implements DenormalizerInterface
{
public function __construct(DenormalizerInterface $delegate)
{
$this->delegate = $delegate;
}
public function denormalize($data, $class, $format = null, array $context = array())
{
if (!is_array($data) || !isset($context['use_attributes']) || true !== $context['use_attributes']) {
return $this->delegate->denormalize($data, $class, $format, $context);
}
$attributes = array();
foreach ($data as $key => $value) {
if (0 === strpos($key, '#')) {
$attributes[substr($key, 1)] = $value;
}
}
if (empty($attributes)) {
$attributes = $data;
}
return $this->delegate->denormalize($attributes, $class, $format, $context);
}
public function supportsDenormalization($data, $type, $format = null)
{
return $this->delegate->supportsDenormalization($data, $type, $format);
}
}
And here is an example of usage
use Symfony\Component\Serializer\Serializer;
use Symfony\Component\Serializer\Encoder\XmlEncoder;
use Symfony\Component\Serializer\Normalizer\GetSetMethodNormalizer;
$xml = '<company id="8888" name="MyCompany"/>';
$encoders = array(new XmlEncoder());
$normalizers = array(new AttributesDenormalizer(new GetSetMethodNormalizer));
$serializer = new Serializer($normalizers, $encoders);
$serializer->deserialize($xml, 'Company', 'xml', array('use_attributes' => true));
Which results in
class Company#13 (2) {
protected $id =>
string(4) "8888"
protected $name =>
string(9) "MyCompany"
}
It's now much easier to serialize XML attributes by using the #SerializeName annotation with '#'.
In your Company entity, when defining $name, add
/**
* #ORM\Column(type="string", length=255)
* #SerializedName('#name')
*/
private $name;
Now when you serialize to XML, it will come out as a property, as expected.
I know the OP was actually asking about deserialization, but hope this helps someone who is searching.
Okay so finally I tried to use the JMSSerializerBundle but my case is too complicated. I got many entities with several ManyToOne relations ; and I got bot standard and attributes values in my XML.
So I'll use your idea : create my complete whole Denormalizer. It will use the decoded XML and read it line by line, doing what it has to do (creating entities with Doctrine).
It's gonna be a huge process but the most simple one.
Thank you.
[EDIT] I finally found a pretty good solution.
I registered the links between XML and my entity setters into a yaml table
company:
#id: setCompanyId
#name : setCompanyName
address:
#city: setAddressCity
#street: setAddressStreet
...
Thanks to that, I can easily read my whole XML and, for each node/attribute value, find the setter name into the table and then do :
$company = new Company;
$company->setterNameFromTable($value);
Quite old, but I got to a much simpler solution using Serializator + xpath in SerializedName annotation, so this could be useful for someone.
Having for example this entry XML:
<root>
<company id="123456"/>
</root>
Whe deserializing into an object you could use this annotation to populate the company id into "id" property:
/**
* #Serializer\SerializedName(name="company/#id")
*/
public ?int $id = null;
PS: tested on Symfony 5.4
Bear with me please as I'm quite new to the OOP concept so I might just be way wrong in my thinking here.
I'm developing a class for some functionality I use quite often and I would like it to be configurable in any new project on initialization. The caveat here is that I'd like to set certain default variables and allow them to stay un-configured if the defaults are alright. Here is a bit of code to try to make the concept a bit clearer.
class someClass{
// Setting parameter defaults
private $param_a = 60;
private $param_b = 100;
/*
* The construct function. What I'd like to do here is make the param_a and param_b optional,
* i.e if it doesn't get set on initialization it takes the defaults from the class.
*/
function __construct($param_a, $param_b, $foo){
// do something ...
}
}
$foo = "some value";
// init example using defaults
$someclass = new someClass($foo); // $param_a and $param_b should be 60 and 100 respectively
// init example using custom options
$someclass = new someClass(40, 110, $foo);
Am I going in the right direction as far as how to set up class configuration? If so, how do I make param_a and param_b optional?
function __construct($foo, $param_a = 60, $param_b = 100){
// do something ...
}
You could supply required method arguments first, and then ones with default parameters afterwards, making them optional.
Then assign these to the class variables inside the constructor.
Another way would be to use func_get_args() and parse this.
You could just make the constructor take a general $args argument and merge it with an array of defaults:
public function __construct($args = array()) {
$args = array_merge(array(
'param_a' => 60,
'param_b' => 100,
'foo' => null
), $args);
foreach($args as $key => $val) {
$this->$key = $val;
}
}