Get newly created Podio-PHP item id from PodioItem:Create - podio

I am trying to create new items in four apps apps via one form -- some the items will become app references. For example, I first create an Entity item, then a Person item and relate the new Entity to the Person via an app reference field in the Person App. Then create a Project and relate both the Person and the Entity to the Project. This was successful until I upgraded to the latest Podio-PHP. In the past, when newly created items were returning item_id as integer then I could pass that into the field as an app reference before the next item was created.
I've been trying to get the item_id as integer so I can do the same, but I keep getting null. Anyway, given the way Podio now returns newly created items as objects, should I be trying this a different way?
$app_id = 1234567;
$fields = new PodioItemFieldCollection(array(
...I am defining external_id and values...
));
$item = new PodioItem(array(
'app' => new PodioApp($app_id),
'fields' => $fields,
));
if( $files ){
$item->files = new PodioCollection($files);
};
$item->save();
$new_id = $item->item_id;
return $new_id;
$new_id is where I'm trying to store the integer so I can pass it along to the next item creation as an app reference.

It's not really handled well in podio-php (it's been handled equally un-well since forever though). If you look at the save method you can see what's going on: https://github.com/podio/podio-php/blob/master/models/PodioItem.php#L58-L72
public function save($options = array()) {
$json_attributes = $this->as_json_without_readonly_fields();
if ($this->id) {
return self::update($this->id, $json_attributes, $options);
}
else {
if ($this->app && $this->app->id) {
return self::create($this->app->id, $json_attributes, $options);
}
else {
throw new PodioMissingRelationshipError('{"error_description":"Item is missing relationship to app"}', null, null);
}
}
}
save() doesn't assign the new item_id to the object itself. This is a bug and it would be good if you created an issue at https://github.com/podio/podio-php/issues so it can be fixed.
For now you can see that save() returns the same as the static create method. So your last three lines needs to be replaced with:
$new_item_placeholder = $item->save();
$item->item_id = $new_item_placeholder->item_id;
return $item->item_id;

Related

Update Document with external object

i have a database containing Song objects. The song class has > 30 properties.
My Music Tagging application is doing changes on a song on the file system.
It then does a lookup in the database using the filename.
Now i have a Song object, which i created in my Tagging application by reading the physical file and i have a Song object, which i have just retrieved from the database and which i want to update.
I thought i just could grab the ID from the database object, replace the database object with my local song object, set the saved id and store it.
But Raven claims that i am replacing the object with a different object.
Do i really need to copy every single property over, like this?
dbSong.Artist = songfromFilesystem.Artist;
dbSong.Album = songfromFileSystem.Album;
Or are there other possibilities.
thanks,
Helmut
Edit:
I was a bit too positive. The suggestion below works only in a test program.
When doing it in my original code i get following exception:
Attempted to associate a different object with id 'TrackDatas/3452'
This is produced by following code:
try
{
originalFileName = Util.EscapeDatabaseQuery(originalFileName);
// Lookup the track in the database
var dbTracks = _session.Advanced.DocumentQuery<TrackData, DefaultSearchIndex>().WhereEquals("Query", originalFileName).ToList();
if (dbTracks.Count > 0)
{
track.Id = dbTracks[0].Id;
_session.Store(track);
_session.SaveChanges();
}
}
catch (Exception ex)
{
log.Error("UpdateTrack: Error updating track in database {0}: {1}", ex.Message, ex.InnerException);
}
I am first looking up a song in the database and get a TrackData object in dbTracks.
The track object is also of type TrackData and i just put the ID from the object just retrieved and try to store it, which gives the above error.
I would think that the above message tells me that the objects are of different types, which they aren't.
The same error happens, if i use AutoMapper.
any idea?
You can do what you're trying: replace an existing object using just the ID. If it's not working, you might be doing something else wrong. (In which case, please show us your code.)
When it comes to updating existing objects in Raven, there are a few options:
Option 1: Just save the object using the same ID as an existing object:
var song = ... // load it from the file system or whatever
song.Id = "Songs/5"; // Set it to an existing song ID
DbSession.Store(song); // Overwrites the existing song
Option 2: Manually update the properties of the existing object.
var song = ...;
var existingSong = DbSession.Load<Song>("Songs/5");
existingSong.Artist = song.Artist;
existingSong.Album = song.Album;
Option 3: Dynamically update the existing object:
var song = ...;
var existingSong = DbSession.Load<Song>("Songs/5");
existingSong.CopyFrom(song);
Where you've got some code like this:
// Inside Song.cs
public virtual void CopyFrom(Song other)
{
var props = typeof(Song)
.GetProperties(System.Reflection.BindingFlags.Public | System.Reflection.BindingFlags.Instance)
.Where(p => p.CanWrite);
foreach (var prop in props)
{
var source = prop.GetValue(other);
prop.SetValue(this, source);
}
}
If you find yourself having to do this often, use a library like AutoMapper.
Automapper can automatically copy one object to another with a single line of code.
Now that you've posted some code, I see 2 things:
First, is there a reason you're using the Advanced.DocumentQuery syntax?
// This is advanced query syntax. Is there a reason you're using it?
var dbTracks = _session.Advanced.DocumentQuery<TrackData, DefaultSearchIndex>().WhereEquals("Query", originalFileName).ToList();
Here's how I'd write your code using standard LINQ syntax:
var escapedFileName = Util.EscapeDatabaseQuery(originalFileName);
// Find the ID of the existing track in the database.
var existingTrackId = _session.Query<TrackData, DefaultSearchIndex>()
.Where(t => t.Query == escapedFileName)
.Select(t => t.Id);
if (existingTrackId != null)
{
track.Id = existingTrackId;
_session.Store(track);
_session.SaveChanges();
}
Finally, #2: what is track? Was it loaded via session.Load or session.Query? If so, that's not going to work, and it's causing your problem. If track is loaded from the database, you'll need to create a new object and save that:
var escapedFileName = Util.EscapeDatabaseQuery(originalFileName);
// Find the ID of the existing track in the database.
var existingTrackId = _session.Query<TrackData, DefaultSearchIndex>()
.Where(t => t.Query == escapedFileName)
.Select(t => t.Id);
if (existingTrackId != null)
{
var newTrack = new Track(...);
newTrack.Id = existingTrackId;
_session.Store(newTrack);
_session.SaveChanges();
}
This means you already have a different object in the session with the same id. The fix for me was to use a new session.

How to get Phalcon to not reload the relation each time I want to access it

I am using Phalcon and have a model Order that has a one-to-many relationship with model OrderAddress. I access those addresses through the following function:
public function getAddresses($params = null) {
return $this->getRelated("addresses", array(
"conditions" => "[OrderAddress].active = 'Y'"
));
}
The OrderAddress model has a public property errors that I do not want persisted to the database. The problem I am having is that everytime I access the getAddresses function, it reloads the object from MySQL which completely wipes the values that I set against that property.
I really only want the OrderAddress models to be loaded once, so that each call to getAddresses doesn't make another trip to the DB- it just iterates over the collection that was already loaded.
Is this possible?
I suppose there's no such option in phalcon, so it has to be implemented in your code.
You could create an additional object property for cached addresses, and return it if it's already been initialized:
protected $cachedAddresses = null;
public function getAddresses($params = null) {
if ($this->cachedAddresses === null) {
$this->cachedAddresses = $this->getRelated("addresses", array(
"conditions" => "[OrderAddress].active = 'Y'"
));
}
return $this->cachedAddresses;
}
This could be a quick solution, but it will be painful to repeat it if you have other relations in your code. So to keep it DRY, you could redefine a 'getRelated' method in base model so it would try to return cached relations, if they already were initialized.
It may look like this:
protected $cachedRelations = [];
public function getRelated($name, $params = [], $useCache = true) {
//generate unique cache object id for current arguments,
//so different 'getRelated' calls will return different results, as expected
$cacheId = md5(serialize([$name, $params]));
if (isset($this->cachedRelations[$cacheId]) && $useCache)
return $this->cachedRelations[$cacheId];
else {
$this->cachedRelations[$cacheId] = parent::getRelated($name, $params);
return $this->cachedRelations[$cacheId];
}
}
Then, you can leave 'getAddresses' method as is, and it will perform only one database query. In case you need to update cached value, pass false as a third parameter.
And, this is completely untested, but even if there're any minor errors, the general logic should be clear.

Object wrappers and forms

I am developing an ecommerce platform and came across a difficulty. Basically, I have in my scenario a Product, Option and OptionValue. A product might have multiple options which might have multiple values. The problem is how to store it in a way that is easy to create and edit.
The problem is I've a direct reference to the OptionValue, which is mutable. I need to keep immutable information about the Option and OptionValue (for example, if a order was made and the color was green, even if this option is changed to lime green, the order must keep showing as green). In that case, I need to save some properties of Option (the option name - "Colors" for example) and of each OptionValue (the value of each option - "red" for example). The way I thought, it would require a structure very similar to the existing structure: a new class ItemOption referencing Option and a ItemOptionValue referencing OptionValue.
So, this was my attempt:
To create a form to display my options I have:
class OptionSelectorType extends AbstractType {
public function buildForm(FormBuilderInterface $builder, array $options)
{
foreach ($options['product']->getOptions() as $option) {
$builder->add($option->getId(), 'choice', array('choice_list' => new ObjectChoiceList($option->getValues());));
}
}
}
I'm using a DataTransformer to convert a collection of OptionValue in a collection of OrderItemOption:
class OrderItemOptionToOptionValueTransformer implements DataTransformerInterface
{
public function transform($lineOptions)
{
if(!$lineOptions) {
return array();
}
$values = array();
foreach($lineOptions as $lineOption) {
$lineOption->getOption()->getId();
$values[$id] = array();
foreach($lineOption->getValues() as $lineOptionValue) {
$values[$id][] = $lineOptionValue->getOptionValue();
}
}
return $values;
}
public function reverseTransform($values)
{
$collection = new ArrayCollection();
foreach($values as $optionId => $optionValues) {
if(!$optionValues) {
continue;
}
$lineOption = new OrderItemOption();
$optionValues = is_array($optionValues) ? $optionValues : array($optionValues);
foreach($optionValues as $optionValue) {
$lineOptionValue = new OrderItemOptionValue();
$lineOptionValue->setOptionValue($optionValue);
$lineOption->addValue($lineOptionValue);
}
$lineOption->setOption($optionValue->getOption());
$collection->add($lineOption);
}
return $collection;
}
}
Finally, my OrderItemType form:
class OrderItemType extends AbstractType
{
public function buildForm(FormBuilderInterface $builder, array $options)
{
$builder->add(
$builder->create('options', 'option_selector', array('options' => $options['options']))
->addModelTransformer(new OrderItemOptionToOptionValueTransformer())
);
}
}
It works but doesn't seem to me a good approach, once the OptionValue will be always recreated, never updated.
How would you do it?
I will keep it simple. You can stick with your structure but instead of transforming things on the fly, keep them at the same state.
What do I mean?
Whenever an Option is created, create the corresponding ItemOption. The same goes with the OptionValue and ItemOptionValue. The relation between the two is a one-to-one connection, whereas the Option and OptionValue don't know the connected ItemOption and ItemOptionValue.
Now if a change occurs to the OptionValue, you can query for the connected ItemOptionValue and change the things you need to change (depends on internal structure).
How to store the connection?
Use whatever persistence method you already use. For the case of a database, store the connection in one table like this:
CREATE TABLE item_option_to_option (
optionID INT(10) NOT NULL,
itemOptionID INT(10) NOT NULL,
UNIQUE KEY (optionID, itemOptionID)
);
If possible I would use Foreign Keys to link the both columns to the corresponding columns in the tables item_option and option. It works the same with OptionValue and ItemOptionValue.
How to handle the change?
Whenever the controller for the OptionValue change is called, simply update the Item* models as well.
If an Option or OptionValue is deleted, it is up to you, if you delete the ItemOption or ItemOptionValue as well.

given a list of objects using C# push them to ravendb without knowing which ones already exist

Given 1000 documents with a complex data structure. for e.g. a Car class that has three properties, Make and Model and one Id property.
What is the most efficient way in C# to push these documents to raven db (preferably in a batch) without having to query the raven collection individually to find which to update and which to insert. At the moment I have to going like so. Which is totally inefficient.
note : _session is a wrapper on the IDocumentSession where Commit calls SaveChanges and Add calls Store.
private void PublishSalesToRaven(IEnumerable<Sale> sales)
{
var page = 0;
const int total = 30;
do
{
var paged = sales.Skip(page*total).Take(total);
if (!paged.Any()) return;
foreach (var sale in paged)
{
var current = sale;
var existing = _session.Query<Sale>().FirstOrDefault(s => s.Id == current.Id);
if (existing != null)
existing = current;
else
_session.Add(current);
}
_session.Commit();
page++;
} while (true);
}
Your session code doesn't seem to track with the RavenDB api (we don't have Add or Commit).
Here is how you do this in RavenDB
private void PublishSalesToRaven(IEnumerable<Sale> sales)
{
sales.ForEach(session.Store);
session.SaveChanges();
}
Your code sample doesn't work at all. The main problem is that you cannot just switch out the references and expect RavenDB to recognize that:
if (existing != null)
existing = current;
Instead you have to update each property one-by-one:
existing.Model = current.Model;
existing.Make = current.Model;
This is the way you can facilitate change-tracking in RavenDB and many other frameworks (e.g. NHibernate). If you want to avoid writing this uinteresting piece of code I recommend to use AutoMapper:
existing = Mapper.Map<Sale>(current, existing);
Another problem with your code is that you use Session.Query where you should use Session.Load. Remember: If you query for a document by its id, you will always want to use Load!
The main difference is that one uses the local cache and the other not (the same applies to the equivalent NHibernate methods).
Ok, so now I can answer your question:
If I understand you correctly you want to save a bunch of Sale-instances to your database while they should either be added if they didn't exist or updated if they existed. Right?
One way is to correct your sample code with the hints above and let it work. However that will issue one unnecessary request (Session.Load(existingId)) for each iteration. You can easily avoid that if you setup an index that selects all the Ids of all documents inside your Sales-collection. Before you then loop through your items you can load all the existing Ids.
However, I would like to know what you actually want to do. What is your domain/use-case?
This is what works for me right now. Note: The InjectFrom method comes from Omu.ValueInjecter (nuget package)
private void PublishSalesToRaven(IEnumerable<Sale> sales)
{
var ids = sales.Select(i => i.Id);
var existingSales = _ravenSession.Load<Sale>(ids);
existingSales.ForEach(s => s.InjectFrom(sales.Single(i => i.Id == s.Id)));
var existingIds = existingSales.Select(i => i.Id);
var nonExistingSales = sales.Where(i => !existingIds.Any(x => x == i.Id));
nonExistingSales.ForEach(i => _ravenSession.Store(i));
_ravenSession.SaveChanges();
}

How to validate >1 field at a time, in a Zend sub-form?

I've created a 3 screen "wizard" using the Zend_Form_SubForm example from the online reference documentation.
The requirement I'm having trouble meeting is this:
If fields 1, 2, & 3 of the first screen are already in the database, notify the user that they are trying to add a duplicate record. Each of those fields has their own validators. Somehow I need to add this "group validator".
So, at its most basic level, I'm trying to do:
if($field_1_not_in_db && $field_2_not_in_db && $field_3_not_in_db){
return true;//validation OK
} else {
return false;//invalid data
}
I am coming up against several issues, though:
1) Because it applies to multiple fields, I don't know which field to attach it to. Error messages appear beside the field they are attached to, so this is important... unless I can get these "multi-field validator" errors to appear at the top of the screen, which would be ideal.
2) My validator is only receiving a single value (the value of the field I attach it to, not the values of the multiple fields it is supposed to validate).
3) I provide a link to the original (non-duplicate) record in the error message, but it escapes the link, and I can't figure out how to get around that.
The setup I'm currently using (below) actually executes fine, but NewPlace validator receives $_POST['city_fk'] as $fields, instead of the desired group of posted values.
$city_fk = new Zend_Form_Element_Select('city_fk');
$cities = array();
$city_fk->setMultiOptions($cities)
->setLabel('City')
->setDescription('The city this place is in')
->setRequired(true);
$v = array(
'place_is_unique' => array(
'NewPlace',
'fields' => array('place_name','phone_number','phone_extension','street','post_code_name'),
)
);
$city_fk->addValidators($v);
$addressSubForm->addElement($city_fk);
class My_Validate_NewPlace extends Zend_Validate_Abstract
{
public function isValid($fields)
{
$result = false;
if(!$result)
{
$this->_error('sorry, this is duplicate data. see it here');
return false;
}
return true;
}
}
This won't help you decide which field to attach the validation to, but...
There is a thing called a "validation context" that helps.
When you create your custom validator or form IF you specify a second optional parameter ($context = null), then Zend will auto-populate this with the entire array of posted data, which you can use to incorporate other fields values into your validation. Here's a very basic example:
$city_name = new Zend_Form_Element_Text('city_name');
$place_name = new Zend_Form_Element_Text('place_name');
$place_name->addValidator('NewPlace');
class My_Validate_NewPlace extends Zend_Validate_Abstract
{
public function isValid($value, **$context = null**)
{
if(trim($value)!='' && trim($context['city_name']) != '')
{
return true;
}
return false;
}
}