We have moved our forum to GitHub Discussions. For questions about Phalcon v3/v4/v5 you can visit here and for Phalcon v6 here.

Stuck on creating / updating relations

I am stuck on creating / updating relations of a model.

So the models I have at this moment are Product model, ProductAttribute model and a Attribute model.

Here is the code for all of them

Product

<?php
namespace Greatmedia\Models\V1;

class Product extends \Phalcon\Mvc\Model
{
    public $id;

    public $name;

    public $price;

    public function getSource()
    {
        return 'product';
    }

    public function initialize()
    {
//         $this->hasMany('id', '\Greatmedia\Models\V1\ProductAttribute', 'product_id', [
//             'alias' => 'attributes'
//         ]);

        $this->hasManyToMany(
            'id',
            '\Greatmedia\Models\V1\ProductAttribute',
            'product_id',
            'attribute_id',
            '\Greatmedia\Models\V1\Attribute',
            'id',
            ['alias' => 'attributes']
        );
    }
}

ProductAttribute

<?php
namespace Greatmedia\Models\V1;

class ProductAttribute extends \Phalcon\Mvc\Model
{
    public $id;

    public $product_id;

    public function getSource()
    {
        return 'product_attribute';
    }

    public function initialize()
    {
        $this->belongsTo('product_id', '\Greatmedia\Models\V1\Product', 'id', [
            'alias' => 'product'
        ]);
        $this->belongsTo('attribute_id', '\Greatmedia\Models\V1\Attribute', 'id', [
            'alias' => 'attribute'
        ]);
    }
}

Attribute

<?php
namespace Greatmedia\Models\V1;

class Attribute extends \Phalcon\Mvc\Model
{
    public $id;

    public $value;

    public $label;

    public function getSource()
    {
        return 'attribute';
    }

    public function initialize()
    {
        $this->hasMany('id', '\Greatmedia\Models\V1\ProductAttribute', 'attribute_id', [
          'alias' => 'product_attribute'
        ]);
    }
}

Nothing really fancy, just as the docs told me to do.

What I am doing is a CLI, now that it self works. Creating and updating products is no issue.

I just can't seem to a)create and b)update attributes.

Here is my ImportTask.

ImportTask

<?php
use Greatmedia\Models\V1\Product;
use Greatmedia\Importer\Repository;
use Phalcon\Mvc\Model;
use Greatmedia\Models\V1\Attribute;

class ImportTask extends \Phalcon\CLI\Task
{
    public function mainAction(array $params = []) {
        if (!empty($params)) {
            $this->importOne();
        }
        else {
            $this->importAll();
        }
    }

    public function importAll()
    {
        $repo = new Repository();
        $repo->attachAll();

        while($repo->valid()) {
            $importer = $repo->current();

            foreach ($importer->getProducts() as $prod) {
                $prod_data  = $prod['product'];
                $attributes = $prod['attributes'];
                $sku        = $prod_data['sku'];

                $product = $this->getBySkuOrNew($sku);
                $this->createAttributes($product, $attributes);

                if ($product->save($prod_data) === false) {
                    echo 'Unable to create product.' . PHP_EOL;
                    $this->printMessages($product);
                    continue;
                }

                echo 'Product stored: ' . $product->id . PHP_EOL;
            }

            $repo->next();
        }
    }

    public function createAttributes(Model $product, array $attributes_arr)
    {
        foreach ($product->getAttributes() as $attribute) {
            foreach ($attributes_arr as $attr) {
                foreach ($attr as $attr_key => $attr_val) {
                    $attribute->{$attr_key} = $attr_val;
                }

                if ($attribute->save() === false) {
                    echo 'Unable to create / update attribute.';
                    $this->printMessages($attribute);
                    continue;
                }

                echo 'Attribute created / updated for product: ' . $product->id;
            }
        }
    }

    public function printMessages(Model $model)
    {
        foreach ($model->getMessages() as $message) {
            echo $message . PHP_EOL;
        }
    }

    public function getBySku($sku)
    {
        return Product::findFirst("sku = '{$sku}'");
    }

    public function getBySkuOrNew($sku)
    {
        $product = $this->getBySku($sku);

        if ($product === false) {
            $product = new Product();
        }

        return $product;
    }
}

If at least there was some error or something, but there is,,, nothing. The products get stored, the product stored message is echo'd and that's it.



4.2k
edited Jul '14

First of all in ProductAttribute you don't have a local field attribute_id that you are using

$this->belongsTo('attribute_id', '\Greatmedia\Models\V1\Attribute', 'id', [
                                    ^
        'alias' => 'attribute'
    ]);


1.5k

I just added it, doesn't do the trick, and not sure if it should help actually. Since defining the public properties would only be beneficial for the performance if i'm not mistaken and not required for using the property it self. If i'm wrong let me know, but that might get of topic. The issue is still not resolved after adding the public proppertie attribte_id.

<?php
namespace Greatmedia\Models\V1;

class ProductAttribute extends \Phalcon\Mvc\Model
{
    public $id;

    public $product_id;

    public $attribute_id;

    public function getSource()
    {
        return 'product_attribute';
    }

    public function initialize()
    {
        $this->belongsTo('product_id', '\Greatmedia\Models\V1\Product', 'id', [
            'alias' => 'product'
        ]);
        $this->belongsTo('attribute_id', '\Greatmedia\Models\V1\Attribute', 'id', [
            'alias' => 'attribute'
        ]);
    }
}


1.5k
edited Jul '14

Here is the refactored task, not done yet but this works much better and the setup is more elegant, importers are now SplObjectStorage objects containing the Product models with it's attributes in the storage, instead of the importers returning a bunch of arrays.

ImportTask

<?php
use Greatmedia\Models\V1\Product;
use Greatmedia\Models\V1\Attribute;
use Greatmedia\Importer\Repository;
use Phalcon\Mvc\Model;

class ImportTask extends \Phalcon\CLI\Task
{
    public function mainAction(array $params = []) {
        if (!empty($params)) {
            $this->importOne();
        }
        else {
            $this->importAll();
        }
    }

    public function importAll()
    {
        $repo = new Repository();
        $repo->attachAll();

        while($repo->valid()) {
            $importer = $repo->current();
            $importer->getProducts();
            $importer->rewind();

            while($importer->valid()) {
                $im_prod = $importer->current();

                if (($product = $this->getBySku($im_prod->sku)) === false) {
                    if ($im_prod->save() === false) {
                        echo 'Unable to create product.' , PHP_EOL;
                        $this->printMessages($im_prod);
                        $importer->next();
                    }

                    echo 'New product stored: ' , $product->id , PHP_EOL;
                }
                else {
                    $product->update((array) $im_prod);
                }

                $importer->next();
            }

            $repo->next();
        }
    }

    public function printMessages(Model $model)
    {
        foreach ($model->getMessages() as $message) {
            echo $message , PHP_EOL;
        }
    }

    public function getBySku($sku)
    {
        return Product::findFirst("sku = '{$sku}'");
    }

    public function getBySkuOrNew($sku)
    {
        $product = $this->getBySku($sku);

        if ($product === false) {
            $product = new Product();
        }

        return $product;
    }
}


98.9k

I suggest you not to use the following the method concatening the conditions:

public function getBySku($sku)
{
    return Product::findFirst("sku = '{$sku}'"); // Possible SQL injection
}

Recommended:

public function getBySku($sku)
{
    return Product::findFirstBySku($sku); 
}

or:

public function getBySku($sku)
{
    return Product::findFirst(array("sku = ?0", "bind" => array($sku))); 
}


1.5k

Ok, i actually tought that Phalcon would do that for me, wasn't expecting that it would just dump the value in the query. I went for the recommended way, thanks.



1.5k
edited Jul '14

Would there be a way to do $product->update($im_prod), where $im_prod would be a new Product model. But the two are the same since they share the SKU, so it's the same product only $product would have to be updated with the values from $im_prod. The update() method only takes an array and if i cast the $im_prod to an array (as in the above snippet) , i get a SQL error that the SKU already exists. Since that is an unique field. Or would you just say, change the $im_prod->id to the one from $product, and call save() on $im_prod instead of $product.

This is somewhat unexpected behavior if you ask me, since i just want to update the $product, and it should not matter that there already is a record with that SKU, since it's that record itself.



1.5k

My logic here is way of. Might have been to long behind the PC. The importer is creating a new Product() but then in my Task i try to find the Product by SKU, statically, so it returns the Product created by the importer and not the product queried from the database.