beta/bx.model

安装: 2,814

依赖: 7

建议者: 0

安全: 0

星星: 10

关注者: 4

分支: 12

开放问题: 4

类型:bitrix-module

1.26.0 2024-06-13 07:18 UTC

README

安装

composer require beta/bx.model

模型

模型反映了项目中使用的实体:信息块元素、hl块元素或任意表的记录。它们是形式化的数据,并提供了一个方便操作数据的接口。

每个模型都必须实现ModelInterface接口,该接口继承自ArrayAccess和IteratorAggregate接口,这意味着可以像操作关联数组一样操作模型。同时继承了CollectionItemInterface接口,该接口要求实现方法

  • assertValueByKey(string $key, $value): bool - 检查键对应的值是否与传递的值匹配
  • hasValueKey(string $key): bool - 检查是否存在键的值
  • getValueByKey(string $key) - 返回键的值

在此模块中,存在对模型接口AbsOptimizedModel的不完整实现,预计将其作为创建模型的基础。

模型描述示例

use Bx\Model\AbsOptimizedModel;

class CatalogProduct extends AbsOptimizedModel
{
    protected function toArray(): array
    {
        return [
            'id' => $this->getId(),
            'name' => $this->getName(),
        ];
    }

    public function getId(): int
    {
        return (int)$this['ID'];
    }

    public function setId(int $id)
    {
        $this['ID'] = $id;
    }

    public function getName(): string
    {
        return (string)$this['NAME'];
    }

    public function setName(string $name)
    {
        $this['NAME'] = $name;
    }
}

模型操作示例

$modelData = [
    'ID' => 11,
    'NAME' => 'Some product name',
];

$product = new CatalogProduct($modelData);
$product->getId();                      // 11
$product->getName();                    // 'Some product name'
$product->setName('New product name');

$product['ID'];                         // 11
$product['NAME'];                       // 'New product name'
$product['NAME'] = 'One more product name';

$product->hasValueKey('ID');            // true
$product->getValueByKey('ID');          // 11
$product->assertValueByKey('ID', 11);   // true
$product->assertValueByKey('ID', 12);   // false

/**
 * Результат:
 * ID - 11
 * NAME - New product name
 */
foreach($product as $key => $value) {
    echo "{$key} - {$value}\n";
}

$product->getApiModel();                // ['id' => 1, 'name' => 'One more product name']
json_encode($product);                  // '{"id": 1, "name": "One more product name"}'

聚合模型

在某些情况下,需要根据某些数据集提供形式化的数据。例如,在一个模型中输出特定商品的总价信息:最大/最小价格、平均值等。为此,定义了AggregateModelInterface接口。同样,也存在对此接口的不完整实现 - BaseAggregateModel。

聚合模型描述示例

use Bx\Model\BaseAggregateModel;

class AggregatePrice extends BaseAggregateModel
{
    protected function toArray(): array
    {
        return [
            'min' => $this->getMin(),
            'max' => $this->getMax(),
            'actual' => $this->getActual(),
            'description' => $this->getDescription(),
        ];
    }

    public function getMin(): ?Price
    {
        $min = null;
        foreach($this->getCollection() as $price) {
            if ($min === null) {
                $min = $price;
                continue;
            }

            if ($min->getValue() > $price->getValue()) {
                $min = $price;
            }
        }

        return $min;
    }

    public function getMax(): ?Price
    {
        $max = null;
        foreach($this->getCollection() as $price) {
            if ($max === null) {
                $max = $price;
                continue;
            }

            if ($max->getValue() < $price->getValue()) {
                $max = $price;
            }
        }

        return $max;
    }

    public function getActual(): ?Price
    {
        foreach($this->getCollection() as $price) {
            if (/** Некоторая логика **/) {
                return $price;   
            }
        }
        return null;
    }

    public function getDescription(): string
    {
        return (string)$this['description'];
    }

    public function setDescription(string $description)
    {
        $this['description'] = $description;
    }
}

聚合模型操作示例

use Bx\Model\ModelCollection;

$price1 = [
    'value' => 1000,
    'currency' => 'RUB',
];
$price2 = [
    'value' => 2000,
    'currency' => 'RUB',
];
$price3 = [
    'value' => 5000,
    'currency' => 'RUB',
];

$priceCollection = new ModelCollection([
    $price1,
    $price2,
    $price3,
], Price::class);

$aggregatePrice = new AggregatePrice($priceCollection, [
    'description' => 'some description',
]);

$aggregatePrice->getApiModel();     // ['min' => ['value' => 1000, 'currency' => 'RUB'], 'max' => ['value' => 5000, 'currency' => 'RUB'], 'actual' => null, 'description' => 'some description']

集合

为了处理同类型数据集,创建了另一个实体 - 集合。集合为处理此类数据集提供方便的接口,包括:过滤、搜索、向集合添加/删除元素、按键选择值...

要实现集合,需要实现CollectionInterface接口或ReadableCollectionInterface接口,如果预计元素集不会更改。集合本身可以包含任何实现CollectionItemInterface接口的对象。在此模块中已经有一个完整的集合实现 - Collection。

集合操作示例

use Bx\Model\Collection;

$priceItem1 = new Price([
    'value' => 1000,
    'currency' => 'RUB',
    'group' = 1,
]);
$priceItem2 = new Price([
    'value' => 2000,
    'currency' => 'RUB',
    'group' = 1,
]);
$priceItem3 = new Price([
    'value' => 4000,
    'currency' => 'RUB',
    'group' = 2,
]);

$collection = new Collection(
    $priceItem1,
    $priceItem2,
    $priceItem3
);

$collection->findByKey('value', 2000);          // $priceItem2
$collection->find(function(Price $price) {      // $priceItem1
    return $price->getValueByKey('value') === 1000 && 
        $price->getValueByKey('currency') === 'RUB'
});

$collection->filterByKey('group', 1);           // вернет новую коллекцию состоящую из $priceItem1 и $priceItem2
$collection->filter(function(Price $price) {    // вернет новую коллекцию состоящую из $priceItem2 и $priceItem3
    return $price->getValueByKey('value') > 1000 && 
        $price->getValueByKey('currency') === 'RUB'
});

$collection->column('value');                   // [1000, 2000, 4000]
$collection->unique('currency');                // ['RUB']
$collection->remove($priceItem2);               // удаляем элемент $priceItem2 из коллекции
$collection->append(new Price([                 // добавляем новый элемент в коллекцию
    'value' => 7000,
    'currency' => 'RUB',
    'group' => 2,
]));

$collection->first();                           // $priceItem1
$collection->count();                           // 3
count($collection);                             // 3

json_encode($collection);                       // JSON представление коллекции
$collection->jsonSerialize();                   // вернет ассоциативный массив

模型集合

在模块中有一个针对模型的集合实现 ModelCollection,该集合的数据可以包含任何实现ModelInterface接口的对象。

使用示例

use Bx\Model\ModelCollection;

$productData1 =[
    'ID' => 11,
    'NAME' => 'Product name 11',
];
$productData2 = [
    'ID' => 21,
    'NAME' => 'Product name 21',
];
$product3 = new CatalogProduct([
    'ID' => 31,
    'NAME' => 'Product name 31',
]);

$productCollection = new ModelCollection([
    $productData1,
    $productData2,
    $product3
], CatalogProduct::class);

$productCollection->addModel(new CatalogProduct([
    'ID' => 41,
    'NAME' => 'Product name 41',
]));

$productCollection->add([
    'ID' => 51,
    'NAME' => 'Product name 51',
]);

MappedCollection & MappedCollectionCallback

为了方便,创建了派生集合,其中可以指定对集合元素的任意键访问。

  • MappedCollection - 接受构造函数中的任意集合,并创建基于指定键的访问映射。
  • MappedCollectionCallback - 接受构造函数中的任意集合,并创建基于可计算键的访问映射。

使用示例

use Bx\Model\MappedCollection;
use Bx\Model\MappedCollectionCallback;

$mappedCollection = new MappedCollection($productCollection, 'ID');
$mappedCollection[41]->getName();                   // Product name 41

$mappedCollectionCallback = new MappedCollectionCallback(
    $productCollection, 
    function(CatalogProduct $product){
        return 'product_'.$product->getId();
    }
);
$mappedCollectionCallback['product_41']->getName(); // Product name 41

服务

为了与数据库集成,使用服务 - ModelServiceInterface,这些服务实现基本操作

  • 查询实体列表(根据特定标准) - 模型列表或集合
  • 查询特定实体 - 模型
  • 添加/更新实体 - 模型
  • 删除实体 - 模型

要实现模型服务,需要实现ModelServiceInterface接口,该接口包含以下

  • QueryableModelServiceInterface
    • ReadableModelServiceInterface - 操作:getList, getCount 和 getById
    • FilterableInterface - 需要指定过滤规则
    • SortableInterface - 需要指定排序规则
    • LimiterInterface - 需要指定选择限制规则
  • SaveableModelServiceInterface - 保存/更新方法
  • RemoveableModelServiceInterface - 删除方法

模块中存在基于BaseModelService创建自定义服务的不完整服务实现。

服务描述示例

use Bx\Modle\BaseModelService;
use Bx\Modle\ModelCollection;

class CatalogProductService extends BaseModelService
{
    protected function getFilterFields(): array
    {
        return [
            // указываем разрешенные для фильтрации поля
        ];
    }

    protected function getSortFields(): array
    {
        return [
            // указываем разрешенные для сортировки поля
        ];
    }

    public function getList(array $params, UserContextInterface $userContext = null): ModelCollection
    { 
        $list = CatalogProductTable::getList($params)->fetchAll(); 
        return new ModelCollection($list, CatalogProduct::class);
    }

    public function getCount(array $params, UserContextInterface $userContext = null): int
    {
        $params['select'] = ['ID'];
        $params['count_total'] = true;
        $params['limit'] = 1;

        return $this->getList($params, $userContext)->first();
    }

    public function getById(int $id, UserContextInterface $userContext = null): ?AbsOptimizedModel;
    {
        $params = [
            'filter' => [
                '=ID' => $id,
            ],
            'limit' => 1,
        ];

        return $this->getList($params, $userContext)->first();
    }

    function save(AbsOptimizedModel $model, UserContextInterface $userContext = null): Result
    {
        $data = [
            'NAME' => $model->getName(), 
        ];

        if ($model->getId() > 0) {
            return CatalogProductTable::update($model->getId(), $data);
        }

        $result = CatalogProductTable::add($data);
        if ($result->isSuccess()) {
            $model['ID'] = $result->getId();
        }

        return $result;
    }

    function delete(int $id, UserContextInterface $userContext = null): Result
    {
        return CatalogProductTable::delete($id);
    }
}

服务使用示例

$catalogProductService = new CatalogProductService();
$productCollection = $catalogProductService->getList([
    'filter' => [
        '=ID' => [1, 5, 10],
    ],
    'select' => [
        'ID',
        'NAME',
    ],
    'order' => [
        'NAME' => 'desc',
    ],
    'limit' => 5,
]);

$productCollection->unique('NAME');
$productCollection->column('ID');

$product = $productCollection->findByKey('ID', 5);
$product->setName('New product name');

$resultSave = $catalogProductService->save($product);
$resultDelete = $catalogProductService->delete(10);

查询

模块中包含从数据库查询参数的对象模型,由QueryInterface接口描述,同时提供了该接口的完整实现 - Query。此类对象可以包含参数:select、filter、order、group、limit、offset。存在派生接口ModelQueryInterface,模块中提供了该接口的完整实现 - QueryModel,该接口直接与模型服务交互,通过构造函数传入服务,并通过双重代理实现数据选择,提供了额外的模型选择方法。

  • loadFiler - 根据传入的关联数组形成过滤器,遵循ModelServiceInterface->getFilterFields()中描述的服务过滤规则。
  • loadSort - 根据传入的关联数组形成数据排序参数,遵循ModelServiceInterface->getSortFields()中描述的服务排序规则。
  • loadPagination - 根据传入的数组形成分页参数,使用键:limit和page。
  • getPagination - 返回PaginationInterface分页对象。
  • getList - 根据形成的查询参数返回模型集合。

使用示例

use Bitrix\Main\Application;

$catalogProductService = new CatalogProductService();
$request = Application::getInstance()->getContext()->getRequest();
$queryParams = $request->getQueryParams();

$query = $catalogProductService->query();   // возвращается объект QueryModel
$query->loadFiler($queryParams)             // загружаем фильтр из http запроса
    ->loadSort($queryParams)                // загружаем параметры сортировки
    ->loadPagination($queryParams);         // загружаем параметры пагинации

$query->hasFilter();                        // проверяет наличие параметров для фильтрации
$query->getFilter();                        // возвращает параметры для фильтрации

$query->hasSort();                          // проверяет наличие параметров для сортировки
$query->getSort();                          // возвращает параметры для сортировки

$query->hasLimit();                         // проверяет наличие параметров для ограничения выборки
$query->getLimit();                         // возвращает параметры ограничения выборки

$query->getPage();                          // номер страницы для пагинации
$query->getOffset();                        // номер элемента с которого начинается выборка

$productCollection = $query->getList();     // возвращает коллекцию товаров в соответствии со сформированными параметрами выборки

分页

为了更方便地处理分页,定义了PaginationInterface接口,模块中提供了该接口的完整实现 - Pagination。该类与QueryInterface接口协同工作。

使用示例

use Bitrix\Main\Application;

$catalogProductService = new CatalogProductService();
$request = Application::getInstance()->getContext()->getRequest();
$queryParams = $request->getQueryParams();

$query = $catalogProductService->query();   // возвращается объект QueryModel
$query->loadFiler($queryParams)             // загружаем фильтр из http запроса
    ->loadSort($queryParams)                // загружаем параметры сортировки
    ->loadPagination($queryParams);         // загружаем параметры пагинации

$pagination = $query->getPagination();      // возвращается объект Pagination
$pagination->getPage();                     // номер текущей страницы
$pagination->getCountPages();               // общее количество страниц
$pagination-getTotalCountElements();        // общее количество элементов
$pagination->getCountElements();            // количество элементов на текущей странице
$pagination->getLimit();                    // максимальное количество элементов на странице

json_encode($pagination);                   // JSON представление
$pagination->toArray();                     // представление в виде ассоциативного массива

抓取器

在许多情况下,需要获取与相关模型关联的数据模型(根据数据库的外键或其他标准),为了简化此任务,定义了FetcherModelInterface接口,该接口有完整的实现 - FetcherModel。该类与模型服务交互,为了更简单的实现,定义了基于此类的抽象模型服务模型BaseLinkedModelService,可以基于此类定义一个用于选择带有相关模型的服务的自定义服务。

服务描述示例

use Bx\Model\BaseLinkedModelService;
use Bx\Model\FetcherModel;
use Bx\Model\Query;
use Bx\Model\Interfaces\FileServiceInterface;

class ExtendedCatalogProductService extends BaseLinkedModelService
{
    /**
     * @var FileServiceInterface
     */
    private $fileService;

    public function __construct(FileServiceInterface $fileService)
    {
        $this->fileService = $fileService;
    }

    protected function getFilterFields(): array
    {
        return [
            // указываем разрешенные для фильтрации поля
        ];
    }

    protected function getSortFields(): array
    {
        return [
            // указываем разрешенные для сортировки поля
        ];
    }

    protected function getLinkedFields(): array
    {
        /**
         * В данном методе описываются внешние связи через FetcherModelInterface
         * в виде ассоциативного массива
         */

        $imageQuery = new Query();
        $imageQuery->setFetchList([]);  // пустой массив указывает на то что связанные модели выбирать не нужно, по-умолчанию выбираются все связанные модели
        $imageQuery->setSelect(['ID', 'SIZE', 'DESCRIPTION']);
        $imageQuery->setFilter('=CONTENT_TYPE' => ['jpg', 'png', 'gif']);

        $docsQuery = new Query();
        $docsQuery->setFetchList([]);
        $docsQuery->setFilter('=CONTENT_TYPE' => ['doc', 'docx', 'pdf']);

        return [
            'image' => FetcherModel::initAsSingleValue( // будет выбрана одна модель
                $this->fileService,
                'image',        // ключ по которому будет доступна связанная модель
                'IMAGE_ID',     // внешний ключ текущей сущности
                'ID',           // первичный ключ связанной сущности
                $imageQuery     // указываем доп. параметры выборки
            ),
            'docs' => FetcherModel::initAsMultipleValue( // будет выбрана коллекция моделей
                $this->fileService,
                'docs',
                'ID',
                'PRODUCT_ID',
                $docsQuery
            )->castTo(AggreageDocumentModel::cass), // выбранная коллекция будет преобразована в указанную агрегационную модель
        ];
    }

    protected function getInternalList(array $params, UserContextInterface $userContext = null): ModelCollection
    { 
        $list = CatalogProductTable::getList($params)->fetchAll(); 
        return new ModelCollection($list, CatalogProduct::class);
    }

    public function getCount(array $params, UserContextInterface $userContext = null): int
    {
        $params['select'] = ['ID'];
        $params['count_total'] = true;
        $params['limit'] = 1;

        return $this->getList($params, $userContext)->first();
    }

    public function getById(int $id, UserContextInterface $userContext = null): ?AbsOptimizedModel;
    {
        $params = [
            'filter' => [
                '=ID' => $id,
            ],
            'limit' => 1,
        ];

        return $this->getList($params, $userContext)->first();
    }

    function save(AbsOptimizedModel $model, UserContextInterface $userContext = null): Result
    {
        $data = [
            'NAME' => $model->getName(), 
        ];

        if ($model->getId() > 0) {
            return CatalogProductTable::update($model->getId(), $data);
        }

        $result = CatalogProductTable::add($data);
        if ($result->isSuccess()) {
            $model['ID'] = $result->getId();
        }

        return $result;
    }

    function delete(int $id, UserContextInterface $userContext = null): Result
    {
        return CatalogProductTable::delete($id);
    }
}

使用示例

use Bx\Model\Services\FileService;

$fileService = new FileService();
$productService = new ExtendedCatalogProductService($fileService);

$collection1 = $productService->getList([]);                       // будут выбраны все связанные модели             
$collection2 = $productService->getList(['fetch' => []]);          // связанные модели не будут выбраны
$collection3 = $productService->getList(['fetch' => ['image']]);   // из связанных моделей будет выбраны только модели с ключом image

$firstModel = $collection1->first();
$firstModel['image'];      // Объект File
$firstModel['docs'];       // Объект AggreageDocumentModel

管理接口

模块中提供了用于在Bitrix的admin界面中快速显示模型列表的工具,具有过滤、搜索、创建自定义复选和单选事件的功能。

使用示例

use Bx\Model\UI\Admin\ModelGrid;

require_once($_SERVER['DOCUMENT_ROOT'].'/bitrix/modules/main/include/prolog_admin_before.php');

$fileService = new FileService();
$productService = new ExtendedCatalogProductService($fileService);
$grid = new ModelGrid(
    $productService,        // указываем сервис для работы с данными
    'product_list',         // указываем символьный идентификатор списка сущности
    'ID'                    // ключ свойства модели для идентификации
);

/**
 * Указываем поля фильтрации
 */
$grid->addSearchFilterField('name', 'Название');
$grid->addNumericFilterField('id', 'ID');
$grid->addBooleanFilterField('active', 'Активность')
          ->setTrueOption('Активно', 'Y')
          ->setFalseOption('Не активно', 'N');
$grid->addStringFilterField('article', 'Артикул');
$grid->addDateFilterField('date_create', 'Дата создания');
$grid->addListFilterField('status', 'Статус', [
    1 => 'Новый',
    2 => 'Опубликован',
    3 => 'Снят с публикации',
]);

/**
 * Указываем колонки для вывода в таблице
 */
$grid->addColumn('id', 'ID');
$grid->addColumn('name', 'Название');
$grid->addColumn('article', 'Артикул');
$grid->addColumn('date_create', 'Дата создания');
$grid->addCalculateColumn(
    'status', 
    function(ExtendedCatalogProduct $product) {
        return $product->getStatusName();
    },
    'Статус'
);

/**
 * Указываем действия над элементами
 */
$grid->setSingleAction('Удалить', 'delete')
    ->setCallback(function (int $id) use ($productService) {
        $productService->delete($id);
    });
$grid->setSingleAction('Перейти', 'redirect')
    ->setJs('location.href="/bitrix/admin/product_detail.php?id=#id#"');

/**
 * Указываем действия над элементами с условием отображения:
 * если callback возвращает false, то действие на элементе не отобразится
 */
$grid->setConditionalSingleAction('Удалить', 'delete')
    ->setShowConditionCallback(function (ExtendedCatalogProduct $model) {
        return !$model->hasValueKey('skuList') || empty($model->getValueByKey('skuList'));
    })
    ->setCallback(function (int $id) use ($productService) {
        $productService->delete($id);
    });

/**
 * Указываем действия над группой элементов
 */
$grid->setGroupAction('Удалить', 'delete')
    ->useConfirm('Подтвердить')
    ->setCallback(function (array $ids) use ($productService) {
        foreach ($ids as $id) {
            $productService->delete((int)$id);
        }
    });
$grid->setGroupAction('Опубликовать', 'accept')
    ->useConfirm('Подтвердить')
    ->setCallback(function (array $ids) use ($productService) {
        $productCollection = $productService->getList([
            'filter' => [
                '=ID' => $ids,
            ],
            'fetch' => [],
        ]);
        foreach ($productCollection as $currentProduct) {
            $currentProduct->setStatus(2);
            $productService->save($currentProduct);
        }
    });

/**
 * Добавляем кнопку в шапку справа от фильтра (вторая и последующие добавятся как выпадающее меню)
 */
$grid->addAdminButtonLink('Добавить', '/bitrix/admin/product_detail.php?lang='.LANG, 'btn_new');

/**
 * Указываем, показывать ли кнопку экспорта в Excel (формирование excel-файла средствами битрикса)
 */
$grid->setShowExcelBtn(true);

/**
 * Добавляем ссылку на строку таблицы (переход по двойному клику мышкой)
 * Если не задать второй аргумент title, по умолчанию будет "Перейти"
 * Если шаблон не подходит, можно использовать setDefaultRowLinkByCallback(callable $fnCalcLink, ?string $linkTitle = null)
 */
$grid->setDefaultRowLinkTemplate('/bitrix/admin/product_detail.php?id=#ID#', 'Изменить');

/**
* Добавляем ссылку на строку таблицы (переход по двойному клику мышкой, альтернативный метод)
*/
$grid->setDefaultRowLinkByCallback(function (ExtendedCatalogProduct $product) {
    return '/bitrix/admin/product_detail.php?id='.$product->getId();
}, 'Изменить');

require($_SERVER['DOCUMENT_ROOT'].'/bitrix/modules/main/include/prolog_admin_after.php');
$grid->show();  // показываем собранную таблицу

require_once($_SERVER["DOCUMENT_ROOT"]."/bitrix/modules/main/include/epilog_admin.php");

基本服务

模块中实现了几个服务。

  • FileService - 允许处理b_file表中的文件。
  • UserService - 允许处理b_user表中的用户。

用户服务

除了标准的服务模型方法之外,还提供了以下方法

  • login(string $login, string $password): ?UserContextInterface - 根据用户名和密码进行认证
  • isAuthorized(): bool - 检查认证状态
  • saveExtendedData(User $user, string ...$keyListForSave): Result - 保存用户和任意数据集
  • updatePassword(User $user, string $password): Result - 更新用户密码
  • getCurrentUser(): ?UserContextInterface - 获取当前用户,实际上返回一个UserContextInterface对象,可以从该对象中获取用户模型。

如方法描述所示,在许多情况下,服务返回的不是模型本身,而是模型上下文 - UserContextInterface,从上下文中可以请求用户的ID或模型本身 - getUserId或getUser。在此接口中,有两个有趣的方法

  • setAccessStrategy(AccessStrategyInterface $accessStrategy) - 允许指定任意的访问策略
  • hasAccessOperation(int $operationId): bool - 检查用户是否可以按照指定的策略执行任意操作

使用示例

use Bx\Model\Services\UserService;

$userService = new UserService();
$userContext = $userService->login('admin@mail.xyz', 'mypassword');
$isAuthorized = $userService->isAuthorized();
$userId = $userContext->getId();

$user = $userContext->getUser();
$user->getId();
$user->getName();

$userContext->setAccessStrategy(new SimpleAccessStrategy());
$userContext->hasAccessOperation(OperationListInterface::CAN_DELETE_FILES);

文件服务

除了标准的服务模型方法之外,还提供了以下方法

  • saveFiles(string $baseDir, string ...$filePaths): ModelCollection - 从指定的url/绝对路径保存文件并返回集合
  • function saveUploadFiles(string $baseDir, UploadedFileInterface ...$files): ModelCollection - 从指定的UploadedFileInterface对象保存文件并返回集合
  • saveFile(string $baseDir, string $filePath, ?string $name = null, ?string $description = null): ?File - 保存文件,可以指定替代名称和描述
  • replaceFile(int $fileId, string $baseDir, string $filePath, ?string $name = null, ?string $description = null): ?File - 删除指定ID的文件并保存新的文件

使用示例

use Bx\Model\Services\FileService;

$fileService = new FileService();
$fileCollection = $fileService->saveFiles(
    'test_dir', 
    'https://some-site.xyz/image1.jpg',
    'https://some-site.xyz/image2.jpg',
    'https://some-site.xyz/image3.jpg' 
);

$savedFile = $fileService->saveFile('test_dir', 'https://some-site.xyz/image1.jpg', 'new_name1.jpg');
$savedFile = $fileService->replaceFile(
    $savedFile->getId(), 
    'test_dir', 
    'https://some-site.xyz/image2.jpg', 
    'new_name2.jpg'
);