XF 2.0 Attachments in custom add-on

mjda

Well-known member
Since I failed for about 6 hours yesterday trying to figure out how to do this any other way, I figured it couldn't be any harder to just use XF's attachment system to accomplish uploading images for my add-on. So, since I suck at coding I was hoping some of you would be able to help me along.

I really do want to learn what's going on here so, if possible, rather than just copy/pasting me some code a little explanation would be a lot better.

Anyways, here's what I put in my template:

<xf:macro template="helper_attach_upload" name="upload" arg-attachmentData="{$attachmentData}" />

I'm trying to just re-use the 'helper_attach_macro' template, which I'm only assuming will work, but it wouldn't work without the attachmentData arg. The problem now is I have no idea what should go into that variable or where I can look to find out.
 
PHP:
        /** @var \XF\Repository\Attachment $attachmentRepo */
        $attachmentRepo = $this->repository('XF:Attachment');
        $attachmentData = $attachmentRepo->getEditorData('your_content_type', $yourEntity);

Make sure you've added a new attachment_handler_class field for your_content_type in the development menu. You also need an entity field for your_content_type in the same menu.

You have to be careful when saving your entity to also associate the attachments with your content:

PHP:
        /** @var \XF\Service\Attachment\Preparer $inserter */
        $inserter = $this->service('XF:Attachment\Preparer');
        $associated = $inserter->associateAttachmentsWithContent($this->filter('attachment_hash'), 'your_content_type', $yourEntity->your_content_id_field);
        if ($associated)
        {
            $yourEntity->fastUpdate('attach_count', $yourEntity->attach_count + $associated);
        }
your_content_id_field must be an auto increment primary key.

Run that code in your controller on save, both after creation and after edit. If it's after creation, make sure it's done after your entity is saved so that the primary key is available.


Fillip
 
PHP:
        /** @var \XF\Repository\Attachment $attachmentRepo */
        $attachmentRepo = $this->repository('XF:Attachment');
        $attachmentData = $attachmentRepo->getEditorData('your_content_type', $yourEntity);

Make sure you've added a new attachment_handler_class field for your_content_type in the development menu. You also need an entity field for your_content_type in the same menu.

You have to be careful when saving your entity to also associate the attachments with your content:

PHP:
        /** @var \XF\Service\Attachment\Preparer $inserter */
        $inserter = $this->service('XF:Attachment\Preparer');
        $associated = $inserter->associateAttachmentsWithContent($this->filter('attachment_hash'), 'your_content_type', $yourEntity->your_content_id_field);
        if ($associated)
        {
            $yourEntity->fastUpdate('attach_count', $yourEntity->attach_count + $associated);
        }
your_content_id_field must be an auto increment primary key.

Run that code in your controller on save, both after creation and after edit. If it's after creation, make sure it's done after your entity is saved so that the primary key is available.


Fillip

Thanks for your help, Fillip. I finally got the attachments at least uploading, and deleting from the form. I also confirmed that they're being uploading into the attachments directory. Now on to figure out how to save all the data and associate the attachments. Thanks again! I tried this for hours yesterday and couldn't come up with anything. :)
 
I noticed when saving a new conversation message XF uses $creator->save(); but when I try to use that to save my data I get an undefined method error. Obviously I'm doing something wrong, but I can't seem to find where.
 
Thanks for the help. When I try it this way I get a cannot access protected property error.

Can you paste your code?

Here is an example of an insert I have.

PHP:
$player_id = $this->filter('player_id', 'int');
$fullname = $this->filter('fullname', 'str');
$height = $this->filter('height', 'int');
$inches = $this->filter('inches', 'int');
$weight = $this->filter('weight', 'int');
$city = $this->filter('city', 'str');
$state = $this->filter('state', 'str');
$school = $this->filter('school', 'str');
$rating = $this->filter('rating', 'int');
$class = $this->filter('class', 'int');

$save = \XF::em()->create('CanesInSight\Backend:Player');
$save->fullname = $fullname;
$save->height = $height;
$save->inches = $inches;
$save->weight = $weight;
$save->city = $city;
$save->state = $state;
$save->school = $school;
$save->rating = $rating;
$save->class = $class;
$save->save();
 
Can you paste your code?

Here is an example of an insert I have.

PHP:
$player_id = $this->filter('player_id', 'int');
$fullname = $this->filter('fullname', 'str');
$height = $this->filter('height', 'int');
$inches = $this->filter('inches', 'int');
$weight = $this->filter('weight', 'int');
$city = $this->filter('city', 'str');
$state = $this->filter('state', 'str');
$school = $this->filter('school', 'str');
$rating = $this->filter('rating', 'int');
$class = $this->filter('class', 'int');

$save = \XF::em()->create('CanesInSight\Backend:Player');
$save->fullname = $fullname;
$save->height = $height;
$save->inches = $inches;
$save->weight = $weight;
$save->city = $city;
$save->state = $state;
$save->school = $school;
$save->rating = $rating;
$save->class = $class;
$save->save();


Thanks for this. I was trying to do everything in a service creator. Once I moved all my code over into my controller it all worked perfectly. I'm not sure why I never tried that before. I guess I just assumed it all had to be done in a service for some reason. Again, thanks for your help! I likely wouldn't have figured this out otherwise.
 
Thanks for this. I was trying to do everything in a service creator. Once I moved all my code over into my controller it all worked perfectly. I'm not sure why I never tried that before. I guess I just assumed it all had to be done in a service for some reason. Again, thanks for your help! I likely wouldn't have figured this out otherwise.
You don't have to use a service, but using a service means you can easily implement this in the front-end since you won't need to copy a lot of controller code.

If you're not going to use a service, look into using the $form = $this->formAction(); - simply having raw code without using a service OR using the FormAction is not really recommended as you will have a harder time validating and displaying error messages.


Fillip
 
You don't have to use a service, but using a service means you can easily implement this in the front-end since you won't need to copy a lot of controller code.

Didn't really think of it like this, but you're right. I guess I'll go back and have a look and see what needs to be done to get this thing working using a service.

Thanks again to both of you for the help, though. I do have the addon successfully inserting data, and attachments, into the db. The attachments are also associating perfectly.
 
Here's a very basic service for creating a Product within a Category. Note that your Category entity must exist and must implement function getNewProduct().

PHP:
    /**
     * @return Product
     */
    public function getNewProduct()
    {
        /** @var Product $product */
        $product = $this->_em->create('Your\Namespace:Product');
        $product->product_category_id = $this->category_id;
       
        return $product;
    }

PHP:
<?php

namespace Your\Namespace\Service\Product;

use Your\Namespace\Entity\Category;

/**
* Class Create
*
* @package Your\Namespace\Service\Product
*/
class Create extends \XF\Service\AbstractService
{
    use \XF\Service\ValidateAndSavableTrait;

    /**
     * @var \Your\Namespace\Entity\Category
     */
    protected $category;
   
    /**
     * @var \Your\Namespace\Entity\Product
     */
    protected $product;
   
    /**
     * @var string
     */
    protected $attachmentHash;
   
    /**
     * @var bool
     */
    protected $performValidations = true;
   
    /**
     * Create constructor.
     *
     * @param \XF\App $app
     * @param Category $category
     */
    public function __construct(\XF\App $app, Category $category)
    {
        parent::__construct($app);
        $this->category = $category;
        $this->setupDefaults();
    }
   
    protected function setupDefaults()
    {
        $product = $this->category->getNewProduct();

        /** @var \Your\Namespace\Entity\Product product */
        $this->product = $product;
    }
   
    /**
     * @return Category
     */
    public function getCategory()
    {
        return $this->category;
    }
   
    /**
     * @return \Your\Namespace\Entity\Product
     */
    public function getProduct()
    {
        return $this->product;
    }
   
    /**
     * @param $perform
     */
    public function setPerformValidations($perform)
    {
        $this->performValidations = (bool)$perform;
    }
   
    /**
     * @return bool
     */
    public function getPerformValidations()
    {
        return $this->performValidations;
    }

    public function setIsAutomated()
    {
        $this->setPerformValidations(false);
    }
   
    /**
     * @param $hash
     */
    public function setAttachmentHash($hash)
    {
        $this->attachmentHash = $hash;
    }

    protected function finalSetup()
    {
    }
   
    /**
     * @return array
     */
    protected function _validate()
    {
        $this->finalSetup();
       
        $product = $this->product;


        $product->preSave();
        $errors = $product->getErrors();

        return $errors;
    }
   
    /**
     * @return \Your\Namespace\Entity\Product
     * @throws \Exception
     * @throws \XF\PrintableException
     */
    protected function _save()
    {
        $product = $this->product;

        $db = $this->db();
        $db->beginTransaction();
       
        $this->beforeInsert();

        $product->save(true, false);
       
        $this->afterInsert();

        $db->commit();

        return $product;
    }
   
    public function beforeInsert()
    {
   
    }
   
    /**
     * @throws \Exception
     * @throws \XF\PrintableException
     */
    public function afterInsert()
    {
        $category = $this->category;
        $product = $this->product;
       
        if ($this->attachmentHash)
        {
            $this->associateAttachments($this->attachmentHash);
        }
    }
   
    /**
     * @param $hash
     */
    protected function associateAttachments($hash)
    {
        $product = $this->product;
       
        /** @var \XF\Service\Attachment\Preparer $inserter */
        $inserter = $this->service('XF:Attachment\Preparer');
        $associated = $inserter->associateAttachmentsWithContent($hash, 'your_content_type', $product->product_id);
        if ($associated)
        {
            $product->fastUpdate('attach_count', $product->attach_count + $associated);
        }
    }
}

Edit service:

PHP:
<?php

namespace Your\Namespace\Service\Product;

use Your\Namespace\Entity\Category;

/**
* Class Edit
*
* @package Your\Namespace\Service\Product
*/
class Edit extends \XF\Service\AbstractService
{
    use \XF\Service\ValidateAndSavableTrait;

    /**
     * @var \Your\Namespace\Entity\Product
     */
    protected $product;
   
    /**
     * @var string
     */
    protected $attachmentHash;
   
    /**
     * @var bool
     */
    protected $performValidations = true;
   
    /**
     * Edit constructor.
     *
     * @param \XF\App $app
     * @param Product $product
     */
    public function __construct(\XF\App $app, Product $product)
    {
        parent::__construct($app);
        $this->product = $product;
    }
   
    /**
     * @return \Your\Namespace\Entity\Product
     */
    public function getProduct()
    {
        return $this->product;
    }
   
    /**
     * @param $perform
     */
    public function setPerformValidations($perform)
    {
        $this->performValidations = (bool)$perform;
    }
   
    /**
     * @return bool
     */
    public function getPerformValidations()
    {
        return $this->performValidations;
    }

    public function setIsAutomated()
    {
        $this->setPerformValidations(false);
    }
   
    /**
     * @param $hash
     */
    public function setAttachmentHash($hash)
    {
        $this->attachmentHash = $hash;
    }
   
    /**
     * @param $logIp
     */
    public function logIp($logIp)
    {
        $this->logIp = $logIp;
    }

    protected function finalSetup()
    {
    }
   
    /**
     * @return array
     */
    protected function _validate()
    {
        $this->finalSetup();
       
        /** @var \Your\Namespace\Entity\Product product */
        $product = $this->product;


        $product->preSave();
        $errors = $product->getErrors();

        return $errors;
    }
   
    /**
     * @return \Your\Namespace\Entity\Product
     * @throws \Exception
     * @throws \XF\PrintableException
     */
    protected function _save()
    {
        $product = $this->product;

        $db = $this->db();
        $db->beginTransaction();
       
        $this->beforeUpdate();

        $product->save(true, false);
       
        $this->afterUpdate();

        $db->commit();

        return $product;
    }
   
    public function beforeUpdate()
    {
   
    }
   
    /**
     * @throws \Exception
     * @throws \XF\PrintableException
     */
    public function afterUpdate()
    {
        $category = $this->category;
        $product = $this->product;
       
        if ($this->attachmentHash)
        {
            $this->associateAttachments($this->attachmentHash);
        }
    }
   
    /**
     * @param $hash
     */
    protected function associateAttachments($hash)
    {
        /** @var \Your\Namespace\Entity\Product $product */
        $product = $this->product;
       
        /** @var \XF\Service\Attachment\Preparer $inserter */
        $inserter = $this->service('XF:Attachment\Preparer');
        $associated = $inserter->associateAttachmentsWithContent($hash, 'your_content_type', $product->product_id);
        if ($associated)
        {
            $product->fastUpdate('attach_count', $product->attach_count + $associated);
        }
    }
}

Obviously you need to modify your service to create setters for each of your fields, f.ex.

PHP:
public function setHeight($height)
{
    $this->product->height = $height;
}
Etc, but this should be enough to get you started.

Remember to add any methods to both the Create and Edit services.


Fillip
 
How can I manipulate the uploaded attachments before saving? For example, I'd like to take one of those uploaded attachments and create a thumbnail for the content. I did find the generate thumbnail function in the attachment preparer, but I'm not sure how to get that data in the controller.
 
Top Bottom