Programmatically Update Option List for Express Object Attribute

Permalink
I have an express object named products that has an attribute named systems which is an Option List Attribute type. The values of this option list were originally manually entered. The options are System 1, System 2, and System 3.

I will be pulling in a spreadsheet where I need to possibly add in more options to that list. So far, I have been able to get the attribute and using the controllers setOptions() function, the original list of the 3 systems gets removed (this is expected from the setOptions function), but the new values are NOT being added in.

I have an array of items that will need to be added to the option list. The array now looks like this:

$newOptions = ['System 1', 'System 2', 'System 3', 'System 4', 'System 5'];


Below is the code I am using to try to add these new options to the attribute.

$products = Express::getObjectByHandle('products');
foreach ($products->getAttributes() as $ak) {
    if ($ak->getAttributeKeyHandle() == 'systems') {
        $type = $ak->getAttributeType();
        $controller = $type->getController(); 
        // NOTE: $controller is the concrete/attributes/select/controller.php file
        $controller->setAttributeKey($ak);
        $options = [];
        foreach ($newOptions as $val) {
            $option = new SelectValueOption();
            $option->setIsEndUserAdded(true);
            $option->setSelectAttributeOptionValue($val);
            if (is_object($option)) {
                $options[] = $option;
            }


As I mentioned, the original options list of the 3 systems is now no longer a part of the system option list, but the 5 new ones are not being added.

There are no php errors in the logs, and nothing shows up in the Reports -> Logs page of the dashboard.

I am running C5 version 5.8.4.3 using PHP v7.0.32

Any ideas on how I can update the option list for this attribute in code?

 
mnakalay replied on at Permalink Reply
mnakalay
The thing is options are not added directly, they are added to an option list and the list is added to the attribute's settings. Look at the function saveKey() in the select attribute controller and you will find everything you need there.
HDMZDevelopment replied on at Permalink Reply
mnakalay,

Thank you for pointing this out. I have looked over the saveKey function, and I do see what it is doing. However, no matter what I try, it still does not work. At first, I thought it had something to do with the $type and $optionList settings since those are not being passed in with the $_POST $data value while using the imput form. However, if I dump the type and the optionlist from the saveKey function when I submit the actual form to add a new attribute, and compare to what I am setting in my code (below), they come out to the be same, (type = Concrete\Core\Entity\Attribute\Key\Settings\SelectSettings, and optionlist =
DoctrineProxies\__CG__\Concrete\Core\Entity\Attribute\Value\Value\SelectValueOptionList)

My array ($newArray) has two new items that need to be added System 4, and System 5 and looks like this

$newArray = ['System 4', 'System'];


To create the option that the saveKey function needs, I am doing this

$displayOrder = 0;
$options = [];
foreach ($newArray as $option) {
    $opt = new SelectValueOption();
    $opt->setSelectAttributeOptionValue($option);
    $opt->setDisplayOrder($displayOrder);
    if (is_object($opt)) {
        $options[] = $opt;
            ++$displayOrder;
        }
    }


I have checked each step, and the $opt is indeed an object so it goes into the $options array.

At the end of this, if I var dump the $options array, I get this

array(2) {
    [0]=>
        object(Concrete\Core\Entity\Attribute\Value\Value\SelectValueOption)#2613 (7) {
            ["avSelectOptionID":protected]=>NULL
            ["list":protected]=>NULL
            ["values":protected]=>NULL
            ["isEndUserAdded":protected]=>bool(false)
            ["isDeleted":protected]=>bool(false)
            ["displayOrder":protected]=>int(0)
            ["value":protected]=>string(8) "System 4"
        }
        [1]=>
            object(Concrete\Core\Entity\Attribute\Value\Value\SelectValueOption)#2336 (7) {
                ["avSelectOptionID":protected]=>NULL
                ["list":protected]=>NULL

This is exactly the same as if I dump the $options array while using the express attribute form.

I then look over the $options array (just like the saveKey function does on line 695 for handling the new options)

// handle adding the options.
foreach ($options as $option) {
    $option->setOptionList($optionList);
    $optionList->getOptions()->add($option);
}
$type->setOptionList($optionList);


In doing the above, nothing happens for the options list. The original ones are still there and the new ones are not shown. There is nothing showing in the Reports -> Logs of the dashboard.

I am hoping that I am 90% there and just need some more to finalize it all.

Any help anyone could give on this would be fantastic.

Thank you again !!
mnakalay replied on at Permalink Reply
mnakalay
I'll look at the rest of your code a little later but for now, one thing you must change is that options list class. You can't use those proxy classes like that, those are generated by the system and might or might not be available.
The correct class is Concrete\Core\Entity\Attribute\Value\Value\SelectValueOptionList
mnakalay replied on at Permalink Reply
mnakalay
Other than the class problem I noted above, the code seems correct. Try that and if it still doesn't work, let me know.
HDMZDevelopment replied on at Permalink Reply
mnakalay

Thanks again. I was questioning that class myself, but since it was the same proxy class that the Attribute form for Option Lists was using, I assumed it was the same.

I used the class you mentioned, but when I var_dump it, even though I get the class, there is noting specific associated with it about this Entity specifically. Perhaps, I need to pass something to the class?

Instead of using the option list class that was the Doctrine proxy, I am using this

$optionList = new \Concrete\Core\Entity\Attribute\Value\Value\SelectValueOptionList();


If I var dump this, instead of a page that loads forever with lots of data (like it did when I was using the proxy class), all I see is this
object(Concrete\Core\Entity\Attribute\Value\Value\SelectValueOptionList)#2613 (2) { ["avSelectOptionListID":protected]=> NULL ["options":protected]=> object(Doctrine\Common\Collections\ArrayCollection)#2336 (1) { ["elements":"Doctrine\Common\Collections\ArrayCollection":private]=> array(0) { } } }

Trying to let it run, I get this error

Doctrine \ ORM \ ORMInvalidArgumentException
A new entity was found through the relationship 'Concrete\Core\Entity\Attribute\Value\Value\SelectValueOption#list' that was not configured to cascade persist operations for entity: Concrete\Core\Entity\Attribute\Value\Value\SelectValueOptionList@000000005de317470000000057fb7931. To solve this issue: Either explicitly call EntityManager#persist() on this unknown entity or configure cascade persist this association in the mapping for example @ManyToOne(..,cascade={"persist"}). If you cannot find out which entity causes the problem implement 'Concrete\Core\Entity\Attribute\Value\Value\SelectValueOptionList#__toString()' to get a clue.

I have added this to below the line that reads:
$optionList->setOptions($options);
$em = \Doctrine\ORM\EntityManagerInterface;
$em->persist($optionList);
$em->flush();

That throws the same error.

I really appreciate your help with this, but I do not want to take up too much of your time. If you have no other ideas, that is fine.

Thanks again !!
Chad
mnakalay replied on at Permalink Best Answer Reply
mnakalay
Try this
$products = Express::getObjectByHandle('products');
$newOptions = ['System 1', 'System 2', 'System 3', 'System 4', 'System 5'];
$em = Database::connection()->getEntityManager();
foreach ($products->getAttributes() as $ak) {
    if (is_object($ak) && 'systems' == $ak->getAttributeKeyHandle()) {
        $existingSettings = $ak->getAttributeKeySettings();
        $existingOptionsList = $existingSettings->getOptionList();
        $displayOrder = $existingOptionsList->count();
        foreach ($newOptions as $option) {
            $opt = new \Concrete\Core\Entity\Attribute\Value\Value\SelectValueOption();
            $opt->setSelectAttributeOptionValue($option);
            $opt->setDisplayOrder($displayOrder);
            $opt->setIsEndUserAdded(true);
            $opt->setOptionList($existingOptionsList);
            if (is_object($opt)) {
HDMZDevelopment replied on at Permalink Reply
mnakalay,

Thank you so much !!! That worked with just a couple of changes.

In case you were interested in the 2 things I had to change, they are:

$displayOrder = $existingOptionsList->count();
// CHANGED TO
$displayOrder = $existingOptionsList->getOptions()->count();


AND

$existingOptionsList->add($opt);
// CHANGED TO
$existingOptionsList->getOptions()->add($opt);


Thank you again so much !!
mnakalay replied on at Permalink Reply
mnakalay
Thanks for the fix :)
smeranda replied on at Permalink Reply
smeranda
I'm trying to use this as a starting point to get a list of the Select Options for an Express entity attribute and output in JSON. Here's what I have, but it doesn't output anything? What am I doing wrong?

$academic_programs = Express::getObjectByHandle('academic_program');
        foreach ($academic_programs->getAttributes() as $ak) {
            if ('academic_program_level' == $ak->getAttributeKeyHandle()) {
                $programSettings = $ak->getAttributeKeySettings();
                $programOptionsList = $programSettings->getOptionList()->getOptions()->getValues();
                return ResponseFactory::json($existingOptionsList);
                exit;
            }
        }
mnakalay replied on at Permalink Reply
mnakalay
You are trying to return $existingOptionsList instead of $programOptionsList :)

Also you have to test it but you might be able to simplify it like this:
$academic_programs = Express::getObjectByHandle('academic_program');
        foreach ($academic_programs->getAttributes() as $ak) {
            if ('academic_program_level' == $ak->getAttributeKeyHandle()) {
                return ResponseFactory::json($ak->getOptions());
                exit;
            }
        }
smeranda replied on at Permalink Reply
smeranda
Thanks for the reply.

I had a typo in my post. However, with the corrected variable name, still no output:

$academic_programs = Express::getObjectByHandle('academic_program');
        foreach ($academic_programs->getAttributes() as $ak) {
            if ('academic_program_level' == $ak->getAttributeKeyHandle()) {
                $programSettings = $ak->getAttributeKeySettings();
                $programOptionsList = $programSettings->getOptionList()->getOptions()->getValues();
                return ResponseFactory::json($programOptionsList);
                exit;
            }
        }


What's interesting is that it returns a JSON object with the correct amount of values (3), but they are all blank. I'm expecting the string name values. Is there another method I'm missing?

[
    { },
    { },
    { }
]


I tried your simpler version and received:

`Call to undefined method Concrete\Core\Entity\Attribute\Key\ExpressKey::getOptions()`
mnakalay replied on at Permalink Reply
mnakalay
OK? Here are 2 possibilities and they should really work this time. The first time is my shorter option modified.
The second one is your option modified as well.
$academic_programs = Express::getObjectByHandle('academic_program');
$options = [];
foreach ($academic_programs->getAttributes() as $ak) {
    if ('academic_program_level' == $ak->getAttributeKeyHandle()) {
        $controller = $ak->getController();
        if ($controller) {
            $options = $controller->getOptions()
        }
        return ResponseFactory::json($options);
        exit;
    }
}


$academic_programs = Express::getObjectByHandle('academic_program');
$options = [];
foreach ($academic_programs->getAttributes() as $ak) {
    if ('academic_program_level' == $ak->getAttributeKeyHandle()) {
        $programSettings = $ak->getAttributeKeySettings();
        $programOptionsList = $programSettings->getOptionList()->getOptions()->getValues();
        foreach ($programOptionsList as $option) {
            $options[] = $option->getSelectAttributeOptionValue();
            // or if you want the display value
            // $options[] = $option->getSelectAttributeOptionDisplayValue();
        }
        return ResponseFactory::json($options);
        exit;
    }
}
smeranda replied on at Permalink Reply
smeranda
Brilliant, that's it. The `$programOptionsList` is a `Concrete\Core\Entity\Attribute\Value\Value\SelectValueOption` object, that's where I was missing the connection.

The hardest thing I have is understanding what class of object is being returned by the various methods.

Thanks for your help!