XF 2.2 How to create custom api endpoints?

Well, I was missing the parameter "product_id" in url, but I can't dump anyway, getting error "Unknown relation or alias api accessed on xf_fox_product". You know why?

btw, this is assertViewableProduct method:

Code:
/**
     * @param int $id
     * @param string|array $with
     *
     * @return \XFRM\Entity\ResourceItem
     *
     * @throws \XF\Mvc\Reply\Exception
     */
    protected function assertViewableProduct($id, $with = 'api')
    {
        return $this->assertViewableApiRecord('FOX\ProductManager:Product', $id, $with);
    }
 
You're passing a $with argument to \XF\Api\Controller\AbstractController::assertViewableApiRecord, which must map to either relations to eager-load (join), or with-aliases. Most core entities define an api with-alias to eager-load the relations necessary for their API results. If you don't have or need one, you can adjust your default argument:

PHP:
protected function assertViewableProduct($id, $with = null)
 
Now getting "assertViewableApiRecord requires the entity of type FOX\\ProductManager:Product to implement canView()", but I guess entity is correct, right?
 
Yeah, assertViewableApiRecord requires the entity to implement a canView method (which is the standard for performing permission checks across XF). You can call assertRecordExists instead if you do not require these checks.
 
Error "API result rules not defined by FOX\\ProductManager:Product. Override setupApiResultData()."
What rules in this case?
 
For your entities, you'll need to add setupApiResultData methods and modify the structure to add 'api' => true to the columns and relations you want to include in the results.
You can leave the method empty if setting 'api' => true on your columns and relations is sufficient.
 
I remember that but I didn't get the point. I created column api with true value, then I set setupApiResultData method, but still getting same error. What you mean 'empty'? Shouldn't return $result?

Code:
public function actionGet(ParameterBag $params)
    {
        $this->assertApiScope('product:read');

        $product = $this->assertViewableProduct($params->product_id);

        $result = [
            'product' => $product->toApiResult(Entity::VERBOSITY_VERBOSE)
        ];
        return $this->setupApiResultData($result);
    }

    public function setupApiResultData(array $result): ApiResult
    {
        return $this->apiResult($result);
    }
 
I remember that but I didn't get the point. I created column api with true value, then I set setupApiResultData method, but still getting same error. What you mean 'empty'? Shouldn't return $result?
If it doesn't return anything, $result is used automatically.You can see \XF\Mvc\Entity\Entity::toApiResult to see how it works.

I set setupApiResultData method, but still getting same error.
The method should be on the entity, not the controller.

In your entity:
PHP:
protected function setupApiResultData(
    \XF\Api\Result\EntityResult $result, 
    int $verbosity = self::VERBOSITY_NORMAL, 
    array $options = []
): void
{
    // this space intentionally left blank
}
In your controller:
PHP:
public function actionGet(ParameterBag $params): \XF\Mvc\Reply\ApiResult
{
    $this->assertApiScope('product:read');

    $product = $this->assertViewableProduct($params->product_id);

    $result = [
        'product' => $product->toApiResult(Entity::VERBOSITY_VERBOSE)
    ];
    return $this->apiResult($result);
}
 
The method should be on the entity, not the controller.
No luck, when on the entity getting this error: Method 'FOX\ProductManager\Entity\Product::setupApiResultData()' is not compatible with method 'XF\Mvc\Entity\Entity::setupApiResultData()'.

Why not compatible? Isn't overriding?
 
Oh sorry, try:
PHP:
protected function setupApiResultData(
    \XF\Api\Result\EntityResult $result,
    $verbosity = self::VERBOSITY_NORMAL,
    array $options = []
)
 
A few minor things:

For requests which modify data, you should return a success response:
PHP:
return $this->apiSuccess([
    'product' => $product->toApiResult(Entity::VERBOSITY_VERBOSE),
]);

And to reduce boilerplate for checking scopes, you can check them in the controller's pre-dispatch method:
PHP:
protected function preDispatchController($action, ParameterBag $params)
{
    $this->assertApiScopeByRequestMethod('product');
}

It will automatically check the read sub-scope for GET requests and the write sub-scope for all other requests for all actions in the controller.
 
Hey @Jeremy P gimme a hand plz! I set scope and everything but whatever I do I got error requested_page_not_found.

Here is my process. I'm testing through insomnia.
User form post: sku: 'abc123' / qty: 10

Here's my function:
It should be an add/edit function, if sku exists just update, else create.

Code:
public function actionPost(): ApiResult
    {
        $this->assertPostOnly();
        $this->assertRequiredApiInput(['sku','qty']);
        $sku = $this->filter('sku', 'str');
        $qty = $this->filter('qty', 'uint');

        $this->assertLicenseExists($sku);

        if ($this->request->exists('sku') == $sku)
        {
            $product = $this->assertProductExists($sku);
        }
        else
        {
            $product = $this->em()->create('FOX\ProductManager:Product');
        }

        $input = $this->filter([
            'sku'             => 'str',
            'product_name'    => 'str',
            'qty'             => 'uint'
        ]);
     
        $input->save();

        return $this->apiSuccess([
            'product' => $product->toApiResult(Entity::VERBOSITY_VERBOSE)
        ]);
    }


    /**
     * @param int $id
     * @param null $with
     * @param null $phraseKey
     * @return \FOX\ProductManager\Entity\Product
     * @throws \XF\Mvc\Reply\Exception
     */
    protected function assertProductExists($id, $with = null, $phraseKey = null)
    {
        return $this->assertRecordExists('FOX\ProductManager:Product', $id, $with, $phraseKey);
    }
 
Anything which calls assertRecordExists will throw an exception with that error if the record does not exist, so it's worth dumping $product or commenting out your assertLicenseExists call to see if it's hitting that. It looks like you require the sku input to do the license assertion, so the conditional around assertProductExists call will always be true.

And you shouldn't need to call assertPostOnly within the API app since the API router already takes HTTP verbs into account.
 
I checked $this->assertLicenseExists($sku); and there is no return, and when I force "1" I can see it.
So how to make assertLicenseExists search for sku, not id?

Another thing. How to compare if request field matches database?
//... $sku = $this->filter('sku', 'str'); if ($this->request->exists('sku') == $sku) //...
 
Last edited:
So how to make assertLicenseExists search for sku, not id?
The assertRecordExists method only does primary key look-ups. You can use assertRecordExistsByUnique to look-up a unique column instead:

PHP:
public function actionPost(): ApiResult
{
    // ...
    $sku = $this->filter('sku', 'str');
    $license = $this->assertLicenseExists('sku', $sku);
    // ...
}

/**
 * @param list<string>|string|null $with
 */
protected function assertLicenseExists(
    ?string $column,
    $id,
    $with = null,
    ?string $phraseKey = null,
): \FOX\ProductManager\Entity\Product
{
    return $this->assertRecordExistsByUnique(
        'FOX\ProductManager:Product',
        $id,
        $column,
        $with,
        $phraseKey
    );
}

Another thing. How to compare if request field matches database?
\XF\Http\Request::exists just checks if an input was present in the request. If you're already looking up the entity with request input, it will only return results which match already. There is no need to check further.
 
Last edited:
Great, it worked, thanks!
Now, about the return, how to customize it? Currently I'm just receiving the id and User (relation).
I need all product fields like sku, product name, qty.

This is the current return:

Code:
{
    "success": true,
    "products": {
        "product_id": 1,
        "User": {
            //...
        }
    }
}

Code:
return $this->apiSuccess([
            'products' => $products->toApiResult(Entity::VERBOSITY_VERBOSE)
        ]);
 
I'm not sure. You can paste the code for your product entity if you want. Alternatively you can try setting the fields manually in the setupApiResultData method:

PHP:
$result->includeColumn('thing');
// ...or...
$result->thing = $this->thing;
 
Last edited:
Top Bottom