fakturoid/fakturoid-php 问题修复 & 功能扩展

解决BUG、新增功能、兼容多环境部署,快速响应你的开发需求

邮箱:yvsm@zunyunkeji.com | QQ:316430983 | 微信:yvsm316

fakturoid/fakturoid-php

Composer 安装命令:

composer require fakturoid/fakturoid-php

包简介

Fakturoid PHP library

README 文档

README

Tests

PHP library for Fakturoid.cz. Please see API for more documentation. New account just for testing API and using separate user (created via "Settings > User account") for production usage is highly recommended.

⚠️ Upgrading? See UPGRADE.md for breaking changes and migration guide.

Content

Versions

Lib. version Fakturoid API PHP
5.x v3 >=8.2
4.x v3 >=8.2
3.x v3 >=8.2
2.x v3 >=8.1
>1.4.x v3 >=7.4 <= 8.2
1.x < 1.4.x v2 >=5.3.0

Installation

The recommended way to install is through Composer:

composer require fakturoid/fakturoid-php

Library requires PHP 8.2 (or later) and ext-json, nyholm/psr7 and psr/http-client extensions.

User agent and HTTP client

You need to create your own client that implements Psr\Http\Client\ClientInterface or you can use symfony/http-client, guzzlehttp/guzzle or other. And you also need to set a default header value for this client, where you need to specify User-Agent.

Creating a client using Guzzle

new \GuzzleHttp\Client(['headers' => ['User-Agent' => 'Bar']])

Create a client using Symfony

In Symfony app you can define in configuration

(new \Symfony\Component\HttpClient\Psr18Client())->withOptions([['headers' => ['User-Agent' => 'Bar']]))

Authorization by OAuth 2.0

Authorization Code Flow

Authorization using OAuth takes place in several steps. We use data obtained from the developer portal as client ID and client secret (Settings → Connect other apps → OAuth 2 for app developers).

First, we offer the user a URL address where he enters his login information. We obtain this using the following method:

$fManager = new \Fakturoid\FakturoidManager(
    \Psr\Http\Client\ClientInterface, // see User agent and HTTP client above
    '{fakturoid-client-id}',
    '{fakturoid-client-secret}',
    null,
    '{your-redirect-uri}'
);
echo '<a href="' . $fManager->getAuthenticationUrl() . '">Link</a>';

After entering the login data, the user is redirected to the specified redirect URI and with the code with which we obtain his credentials. We process the code as follows:

$fManager->requestCredentials($_GET['code']);

Credentials are now established in the object instance and we can send queries to the Fakturoid api. Credentials can be obtained in 2 ways. Obtaining credentials directly from the object:

$credentials = $fManager->getCredentials();
echo $credentials->toJson();

Client Credentials Flow

$fManager = new \Fakturoid\FakturoidManager(
    \Psr\Http\Client\ClientInterface, // see User agent and HTTP client above
    '{fakturoid-client-id}',
    '{fakturoid-client-secret}'
);
$fManager->authClientCredentials();

Processing credentials using the credentials callback:

The way callback works is that the library calls the callback function whenever the credentials are changed. This is useful because the token is automatically refreshed after its expiration.

$fManager->setCredentialsCallback(new class implements \Fakturoid\Auth\CredentialCallback {
    public function __invoke(?\Fakturoid\Auth\Credentials $credentials = null): void
    {
        // Save credentials to database or another storage
    }
});

Usage

Set credentials to the Fakturoid manager

If you run a multi-tenant application or an application that processes documents in parallel, you need to set Credentials correctly. Each time a new access token is obtained, the previous one is invalidated. For these needs there is AuthProvider::setCredentials() and also CredentialCallback.

$fManager = new \Fakturoid\FakturoidManager(
    \Psr\Http\Client\ClientInterface, // see User agent and HTTP client above
    '{fakturoid-client-id}',
    '{fakturoid-client-secret}'
);
// restore credentials from storage
$credentials = new \Fakturoid\Auth\Credentials(
    'refreshToken',
    'accessToken',
    (new DateTimeImmutable())->modify('-2 minutes'),
    \Fakturoid\Enum\AuthTypeEnum::AUTHORIZATION_CODE_FLOW // or \Fakturoid\Enum\AuthTypeEnum:CLIENT_CREDENTIALS_CODE_FLOW
);

$fManager->getAuthProvider()->setCredentials($credentials);
$fManager->setCredentialsCallback(new class implements \Fakturoid\Auth\CredentialCallback {
    public function __invoke(?\Fakturoid\Auth\Credentials $credentials = null): void
    {
        // Save credentials to database or another storage
    }
});

Switch account

$fManager = new \Fakturoid\FakturoidManager(
    \Psr\Http\Client\ClientInterface, // see User agent and HTTP client above
    '{fakturoid-client-id}',
    '{fakturoid-client-secret}',
    '{fakturoid-account-slug}',
);
$fManager->authClientCredentials();
$fManager->getBankAccountsProvider()->list();

// switch account and company
$fManager->setAccountSlug('{fakturoid-account-slug-another}');
$fManager->getBankAccountsProvider()->list();

Basic usage

require __DIR__ . '/vendor/autoload.php';
$fManager = new \Fakturoid\FakturoidManager(
    \Psr\Http\Client\ClientInterface, // see User agent and HTTP client above
    '{fakturoid-client-id}',
    '{fakturoid-client-secret}'
);
$fManager->authClientCredentials();

// get current user
$user = $fManager->getUsersProvider()->getCurrentUser();
$fManager->setAccountSlug($user->getBody()->accounts[0]->slug);
// or you can set account slug manually
$fManager->setAccountSlug('{fakturoid-account-slug}');

// create subject
$response = $fManager->getSubjectsProvider()->create(['name' => 'Firma s.r.o.', 'email' => 'aloha@pokus.cz']);
$subject  = $response->getBody();

// create invoice with lines
$lines    = [['name' => 'Big sale', 'quantity' => 1, 'unit_price' => 1000]];
$response = $fManager->getInvoicesProvider()->create(['subject_id' => $subject->id, 'lines' => $lines]);
$invoice  = $response->getBody();

// send by mail
$fManager->getInvoicesProvider()->createMessage($invoice->id, ['email' => 'aloha@pokus.cz']);

// to mark invoice as paid and send thank you email
$fManager->getInvoicesProvider()->createPayment($invoice->id, ['paid_on' => (new \DateTime())->format('Y-m-d'), 'send_thank_you_email' => true]);

// lock invoice (other fire actions are described in the API documentation)
$fManager->getInvoicesProvider()->fireAction($invoice->id, 'lock');

Downloading an invoice PDF

$invoiceId = 123;
$response = $fManager->getInvoicesProvider()->getPdf($invoiceId);
$data = $response->getBody();
file_put_contents("{$invoiceId}.pdf", $data);

If you call $fManager->getInvoicesProvider()->getPdf() right after creating an invoice, you'll get a status code 204 (No Content) with empty body, this means the invoice PDF hasn't yet been generated and you should try again a second or two later.

More info in API docs.

$invoiceId = 123;

// This is just an example, you may want to do this in a background job and be more defensive.
while (true) {
    $response = $fManager->getInvoicesProvider()->getPdf($invoiceId);

    if ($response->getStatusCode() == 200) {
        $data = $response->getBody();
        file_put_contents("{$invoiceId}.pdf", $data);
        break;
    }

    sleep(1);
}

Using custom_id

You can use custom_id attribute to store your application record ID into our record. Invoices and subjects can be filtered to find a particular record:

$response = $fManager->getSubjectsProvider()->list(['custom_id' => '10']);
$subjects = $response->getBody();
$subject  = null;

if (count($subjects) > 0) {
    $subject = $subjects[0];
}

As for subjects, Fakturoid won't let you create two records with the same custom_id so you don't have to worry about multiple results. Also note that the field always returns a string.

InventoryItem resource

To get all inventory items:

$fManager->getInventoryItemsProvider()->list();

To filter inventory items by certain SKU code or article number:

$fManager->getInventoryItemsProvider()->list(['sku' => 'SKU1234']);
$fManager->getInventoryItemsProvider()->list(['article_number' => 'IAN321']);

To search inventory items (searches in name, article_number and sku):

$fManager->getInventoryItemsProvider()->listArchived(['query' => 'Item name']);

To get all archived inventory items:

$fManager->getInventoryItemsProvider()->listArchived();

To get a single inventory item:

$fManager->getInventoryItemsProvider()->get($inventoryItemId);

To create an inventory item:

$data = [
    'name' => 'Item name',
    'sku' => 'SKU12345',
    'track_quantity' => true,
    'quantity' => 100,
    'native_purchase_price' => 500,
    'native_retail_price' => 1000
];
$fManager->getInventoryItemsProvider()->create($data)

To update an inventory item:

$fManager->getInventoryItemsProvider()->update($inventoryItemId, ['name' => 'Another name']);

To archive an inventory item:

$fManager->getInventoryItemsProvider()->archive($inventoryItemId);

To unarchive an inventory item:

$fManager->getInventoryItemsProvider()->unArchive($inventoryItemId);

To delete an inventory item:

$fManager->getInventoryItemsProvider()->delete($inventoryItemId);

InventoryMove resource

To get get all inventory moves across all inventory items:

$fManager->getInventoryMovesProvider()->list()

To get inventory moves for a single inventory item:

$fManager->getInventoryMovesProvider()->list(['inventory_item_id' => $inventoryItemId]);

To get a single inventory move:

$fManager->getInventoryMovesProvider()->get($inventoryItemId, $inventoryMoveId);

To create a stock-in inventory move:

$fManager->getInventoryMovesProvider()->create(
    $inventoryItemId,
    [
        'direction' => 'in',
        'moved_on' => '2023-01-12',
        'quantity_change' => 5,
        'purchase_price' => '249.99',
        'purchase_currency' => 'CZK',
        'private_note' => 'Bought with discount'
    ]
)

To create a stock-out inventory move:

$fManager->getInventoryMovesProvider()->create(
    $inventoryItemId,
    [
        'direction' => 'out',
        'moved_on' => '2023-01-12',
        'quantity_change' => '1.5',
        'retail_price' => 50,
        'retail_currency' => 'EUR',
        'native_retail_price' => '1250'
    ]
);

To update an inventory move:

$fManager->getInventoryMovesProvider()->update($inventoryItemId, $inventoryMoveId, ['moved_on' => '2023-01-11']);

To delete an inventory move:

$fManager->getInventoryMovesProvider()->update($inventoryItemId, $inventoryMoveId);

Recurring Generators

To pause a recurring generator:

$fManager->getRecurringGeneratorsProvider()->pause($generatorId);

To activate a paused recurring generator:

$fManager->getRecurringGeneratorsProvider()->activate($generatorId);

To activate with a specific next occurrence date:

$fManager->getRecurringGeneratorsProvider()->activate($generatorId, [
    'next_occurrence_on' => '2025-02-15'
]);

Rate Limiting

Fakturoid implements rate limiting based on RFC Draft. The library provides methods to check rate limit status from the response object:

try {
    $response = $fManager->getInvoicesProvider()->list();
    
    // Get maximum number of requests allowed in the time window
    $quota = $response->getRateLimitQuota(); // e.g., 400
    
    // Get time window in seconds
    $window = $response->getRateLimitWindow(); // e.g., 60
    
    // Get remaining number of requests
    $remaining = $response->getRateLimitRemaining(); // e.g., 398
    
    // Get remaining time in seconds until the rate limit resets
    $resetTime = $response->getRateLimitReset(); // e.g., 55
    
} catch (\Fakturoid\Exception\ClientErrorException $exception) {
    // Check if rate limit was exceeded (status code 429)
    if ($exception->isRateLimitExceeded()) {
        // Access rate limit info from the exception's response
        $response = $exception->getResponse();
        $resetTime = $response->getRateLimitReset();
        
        // Wait until the rate limit resets and retry
        sleep($resetTime);
        // retry the request...
    }
}

When the rate limit is exceeded, the server responds with status code 429 Too Many Requests and throws a ClientErrorException. You can check if the exception is due to rate limiting using isRateLimitExceeded() method and get rate limit details from the response object.

Handling errors

Library raises Fakturoid\Exception\ClientErrorException for 4xx and Fakturoid\Exception\ServerErrorException for 5xx status. You can get response code and response body by calling getCode() or getResponse()->getBody() or getResponse()->getBody() or getResponse()->getBody().

try {
    $response = $fManager->getSubjectsProvider()->create(['name' => '', 'email' => 'aloha@pokus.cz']);
    $subject  = $response->getBody();
} catch (\Fakturoid\Exception\ClientErrorException $e) {
    $e->getCode(); // 422
    $e->getMessage(); // Unprocessable entity
    $e->getResponse()->getBody(); // '{"errors":{"name":["je povinná položka","je příliš krátký/á/é (min. 2 znaků)"]}}'
} catch (\Fakturoid\Exception\ServerErrorException $e) {
    $e->getCode(); // 503
    $e->getMessage(); // Fakturoid is in read only state
}

Common problems

Development

  • To run tests, PHPUnit requires ext-dom extension (typically a php-xml package on Debian) and ext-mbstring extension (php-mbstring package).
  • If you wish to generate code coverage (and have more intelligent stack traces), you will need Xdebug (php-xdebug package), it will hook itself into PHPUnit automatically.

Docker

$ docker-compose up -d
$ docker-compose exec php composer install
$ docker-compose exec php bash

Testing

Both commands do the same but the second version is a bit faster.

$ docker-compose exec php composer test:phpunit
$ docker-compose exec php composer coverage:phpunit
# or locally
$ composer test:phpunit
$ composer coverage:phpunit

Code-Style Check

Both commands do the same but the second version seems to have a more intelligent output.

$ docker-compose exec php composer check:cs
# or locally
$ composer check:cs

Check all requires for PR

$ docker-compose exec php composer check:all
# or locally
$ composer check:all

Or you can fix CS and Rector issues automatically:

$ docker-compose exec php composer fix:all
# or locally
$ composer fix:all

fakturoid/fakturoid-php 适用场景与选型建议

fakturoid/fakturoid-php 是一款 基于 PHP 开发的 Composer 扩展包,目前已累计 406.03k 次下载、GitHub Stars 达 36, 最近一次更新时间为 2014 年 05 月 19 日, 在 PHP 生态内属于活跃度较高的组件。

它主要适用于以下技术方向: 「api」 「invoice」 「fakturoid」 等业务场景。在实际项目中,围绕这些方向常见需要落地的问题包括:接口对接、性能调优、并发安全、与既有框架(Laravel / ThinkPHP / Yii / Webman 等)的兼容适配,以及生产环境的日志埋点与稳定性保障。

我们在过去多个企业项目中使用过 fakturoid/fakturoid-php 或与其功能相近的方案,如果你在选型或落地过程中遇到问题,例如 版本兼容、二次改造、私有化封装、与内部系统对接、生产 BUG 排查,欢迎联系我们协助评估。

围绕 fakturoid/fakturoid-php 我们能提供哪些服务?
定制开发 / 二次开发

基于 fakturoid/fakturoid-php 在你已有业务上做功能扩展、字段裁剪、UI 适配、与内部账号 / 权限 / 日志系统的深度对接。

BUG 修复 & 性能优化

线上偶发问题、内存泄漏、慢查询、并发异常等排查修复;针对高流量场景做缓存、队列、索引层面的调优。

项目外包 & 长期维护

承接完整的项目从需求 → 设计 → 开发 → 上线 → 长期运维;也可按月提供技术保姆服务。

yvsm@zunyunkeji.com QQ:316430983 微信:yvsm316 西安尊云信息科技 · 专注 PHP / Go / 分布式系统研发

统计信息

  • 总下载量: 406.03k
  • 月度下载量: 0
  • 日度下载量: 0
  • 收藏数: 37
  • 点击次数: 18
  • 依赖项目数: 4
  • 推荐数: 0

GitHub 信息

  • Stars: 36
  • Watchers: 16
  • Forks: 21
  • 开发语言: PHP

其他信息

  • 授权协议: MIT
  • 更新时间: 2014-05-19