教程:Hyperf
参考:
hyperf 十、分页-CSDN博客
illuminate/database 使用 一_illuminate db类-CSDN博客
hyperf 二十二 数据库 模型关系-CSDN博客
根据文档,可以使用model的paginate()和query的paginate()。根据源码model中BelongsToMany和HasManyThrough都有paginate()方法,都调用Hyperf\Database\Query\Builder::paginate()。
在Hyperf\Database\Model\Model中__call()调用Hyperf\Database\Query\Builder方法,__callStatic()调用自身。所以Model::paginate(),就是先调用__callStatic(),再调用__call(),最后执行Hyperf\Database\Query\Builder::paginate()。
像教程里写的Model::where('gender',1)->paginate(10)其实也是用的Hyperf\Database\Query\Builder类中的方法。
和分页器相比,这种分页查询改不了每个分页对应路径。分页查询返回Hyperf\Paginator\LengthAwarePaginator类,可以继续操作,但是也不能修改path。
使用DB直接调用paginate,也是调用Hyperf\Database\Query\Builder,所以最后返回的也是Hyperf\Paginator\LengthAwarePaginator类。
$info = User::paginate(2)->toArray();
var_dump($info);
$info = Db::table('userinfo')->paginate(2)->jsonSerialize();
var_dump($info);
测试结果
array(12) {
["current_page"]=>
int(1)
["data"]=>
array(2) {
[0]=>
array(4) {
["id"]=>
int(1)
["name"]=>
string(3) "123"
["age"]=>
int(22)
["deleted_at"]=>
NULL
}
[1]=>
array(4) {
["id"]=>
int(2)
["name"]=>
string(5) "name2"
["age"]=>
int(13)
["deleted_at"]=>
NULL
}
}
["first_page_url"]=>
string(46) "http://127.0.0.1:9501/test/testpaginate?page=1"
["from"]=>
int(1)
["last_page"]=>
int(10)
["last_page_url"]=>
string(47) "http://127.0.0.1:9501/test/testpaginate?page=10"
["next_page_url"]=>
string(46) "http://127.0.0.1:9501/test/testpaginate?page=2"
["path"]=>
string(39) "http://127.0.0.1:9501/test/testpaginate"
["per_page"]=>
int(2)
["prev_page_url"]=>
NULL
["to"]=>
int(2)
["total"]=>
int(19)
}
array(12) {
["current_page"]=>
int(1)
["data"]=>
array(2) {
[0]=>
object(stdClass)#1120 (4) {
["id"]=>
int(1)
["name"]=>
string(3) "123"
["age"]=>
int(22)
["deleted_at"]=>
NULL
}
[1]=>
object(stdClass)#1116 (4) {
["id"]=>
int(2)
["name"]=>
string(5) "name2"
["age"]=>
int(13)
["deleted_at"]=>
NULL
}
}
["first_page_url"]=>
string(46) "http://127.0.0.1:9501/test/testpaginate?page=1"
["from"]=>
int(1)
["last_page"]=>
int(10)
["last_page_url"]=>
string(47) "http://127.0.0.1:9501/test/testpaginate?page=10"
["next_page_url"]=>
string(46) "http://127.0.0.1:9501/test/testpaginate?page=2"
["path"]=>
string(39) "http://127.0.0.1:9501/test/testpaginate"
["per_page"]=>
int(2)
["prev_page_url"]=>
NULL
["to"]=>
int(2)
["total"]=>
int(20)
}
model调用
#Hyperf\Database\Model\Model
public function __call($method, $parameters) {
if (in_array($method, ['increment', 'decrement'])) {
return $this->{$method}(...$parameters);
}
return call([$this->newQuery(), $method], $parameters);
}
public static function __callStatic($method, $parameters) {
return (new static())->{$method}(...$parameters);
}
#Hyperf\Database\Query\Builder
public function paginate($perPage = 15, $columns = ['*'], $pageName = 'page', $page = null): LengthAwarePaginatorInterface {
$page = $page ?: Paginator::resolveCurrentPage($pageName);
$total = $this->getCountForPagination();
$results = $total ? $this->forPage($page, $perPage)->get($columns) : collect();
return $this->paginator($results, $total, $perPage, $page, [
'path' => Paginator::resolveCurrentPath(),
'pageName' => $pageName,
]);
}
protected function paginator(Collection $items, int $total, int $perPage, int $currentPage, array $options): LengthAwarePaginatorInterface {
$container = ApplicationContext::getContainer();
if (!method_exists($container, 'make')) {
throw new \RuntimeException('The DI container does not support make() method.');
}
return $container->make(LengthAwarePaginatorInterface::class, compact('items', 'total', 'perPage', 'currentPage', 'options'));
}
其实就是系统运行位置的钩子,设置监听时使用。
Hyperf\Database\Events\QueryExecuted | Query 语句执行后 |
Hyperf\Database\Events\StatementPrepared | SQL 语句 prepared 后 |
Hyperf\Database\Events\TransactionBeginning | 事务开启后 |
Hyperf\Database\Events\TransactionCommitted | 事务提交后 |
Hyperf\Database\Events\TransactionRolledBack | 事务回滚后 |
设置监听
namespace App\Listener;
use Hyperf\Database\Events\QueryExecuted;
use Hyperf\Event\Annotation\Listener;
use Hyperf\Event\Contract\ListenerInterface;
use Hyperf\Logger\LoggerFactory;
use Hyperf\Utils\Arr;
use Hyperf\Utils\Str;
use Psr\Container\ContainerInterface;
use Psr\Log\LoggerInterface;
/**
* @Listener
*/
class DbQueryExecutedListener implements ListenerInterface
{
/**
* @var LoggerInterface
*/
private $logger;
public function __construct(ContainerInterface $container)
{
$this->logger = $container->get(LoggerFactory::class)->get('sql');
}
public function listen(): array
{
return [
QueryExecuted::class,
];
}
/**
* @param QueryExecuted $event
*/
public function process(object $event)
{
if ($event instanceof QueryExecuted) {
$sql = $event->sql;
if (! Arr::isAssoc($event->bindings)) {
foreach ($event->bindings as $key => $value) {
$sql = Str::replaceFirst('?', "'{$value}'", $sql);
}
}
$this->logger->info(sprintf('[%s] %s', $event->time, $sql));
}
}
例子中是从源码复制,自定义需要在\config\autoload\listeners.php中设置。
钩子函数
事件名 | 触发实际 | 是否阻断 | 备注 |
---|---|---|---|
booting | 模型首次加载前 | 否 | 进程生命周期中只会触发一次 |
booted | 模型首次加载后 | 否 | 进程生命周期中只会触发一次 |
retrieved | 填充数据后 | 否 | 每当模型从 DB 或缓存查询出来后触发 |
creating | 数据创建时 | 是 | |
created | 数据创建后 | 否 | |
updating | 数据更新时 | 是 | |
updated | 数据更新后 | 否 | |
saving | 数据创建或更新时 | 是 | |
saved | 数据创建或更新后 | 否 | |
restoring | 软删除数据恢复时 | 是 | |
restored | 软删除数据恢复后 | 否 | |
deleting | 数据删除时 | 是 | |
deleted | 数据删除后 | 否 | |
forceDeleting | 数据强制删除时 | 是 | |
forceDeleted | 数据强制删除后 | 否 |
测试参考:hyperf 二十二 数据库 模型关系-CSDN博客中测试中设置的自定义多态关系的监听。
#Hyperf\Database\Connection
use Concerns\ManagesTransactions;
protected function fireModelEvent(string $event): ?object
{
$dispatcher = $this->getEventDispatcher();
if (! $dispatcher instanceof EventDispatcherInterface) {
return null;
}
$result = $this->fireCustomModelEvent($event);
// If custom event does not exist, the fireCustomModelEvent() method will return null.
if (! is_null($result)) {
return $result;
}
// If the model is not running in Hyperf, then the listener method of model will not bind to the EventDispatcher automatically.
$eventName = $this->getDefaultEvents()[$event];
return $dispatcher->dispatch(new $eventName($this, $event));
}
protected function getDefaultEvents(): array
{
return [
'booting' => Booting::class,
'booted' => Booted::class,
'retrieved' => Retrieved::class,
'creating' => Creating::class,
'created' => Created::class,
'updating' => Updating::class,
'updated' => Updated::class,
'saving' => Saving::class,
'saved' => Saved::class,
'restoring' => Restoring::class,
'restored' => Restored::class,
'deleting' => Deleting::class,
'deleted' => Deleted::class,
'forceDeleted' => ForceDeleted::class,
];
}
public function logQuery(string $query, array $bindings, ?float $time = null, $result = null)
{
$this->event(new QueryExecuted($query, $bindings, $time, $this, $result));
if ($this->loggingQueries) {
$this->queryLog[] = compact('query', 'bindings', 'time');
}
}
protected function prepared(PDOStatement $statement)
{
$statement->setFetchMode($this->fetchMode);
$this->event(new Events\StatementPrepared(
$this,
$statement
));
return $statement;
}
protected function fireConnectionEvent($event)
{
if (! isset($this->events)) {
return;
}
switch ($event) {
case 'beganTransaction':
return $this->events->dispatch(new Events\TransactionBeginning($this));
case 'committed':
return $this->events->dispatch(new Events\TransactionCommitted($this));
case 'rollingBack':
return $this->events->dispatch(new Events\TransactionRolledBack($this));
}
}
protected function run(string $query, array $bindings, Closure $callback)
{
$this->reconnectIfMissingConnection();
$start = microtime(true);
// Here we will run this query. If an exception occurs we'll determine if it was
// caused by a connection that has been lost. If that is the cause, we'll try
// to re-establish connection and re-run the query with a fresh connection.
try {
$result = $this->runQueryCallback($query, $bindings, $callback);
} catch (QueryException $e) {
$result = $this->handleQueryException(
$e,
$query,
$bindings,
$callback
);
}
// Once we have run the query we will calculate the time that it took to run and
// then log the query, bindings, result and execution time so we will report them on
// the event that the developer needs them. We'll log time in milliseconds.
$this->logQuery(
$query,
$bindings,
$this->getElapsedTime($start),
$result
);
return $result;
}
public function select(string $query, array $bindings = [], bool $useReadPdo = true): array
{
return $this->run($query, $bindings, function ($query, $bindings) use ($useReadPdo) {
if ($this->pretending()) {
return [];
}
// For select statements, we'll simply execute the query and return an array
// of the database result set. Each element in the array will be a single
// row from the database table, and will either be an array or objects.
$statement = $this->prepared($this->getPdoForSelect($useReadPdo)
->prepare($query));
$this->bindValues($statement, $this->prepareBindings($bindings));
$statement->execute();
return $statement->fetchAll();
});
}
#Hyperf\Database\Concerns\ManagesTransactions
public function beginTransaction(): void
{
$this->createTransaction();
++$this->transactions;
$this->fireConnectionEvent('beganTransaction');
}
public function commit(): void
{
if ($this->transactions == 1) {
$this->getPdo()->commit();
}
$this->transactions = max(0, $this->transactions - 1);
$this->fireConnectionEvent('committed');
}
public function rollBack($toLevel = null): void
{
// We allow developers to rollback to a certain transaction level. We will verify
// that this given transaction level is valid before attempting to rollback to
// that level. If it's not we will just return out and not attempt anything.
$toLevel = is_null($toLevel)
? $this->transactions - 1
: $toLevel;
if ($toLevel < 0 || $toLevel >= $this->transactions) {
return;
}
// Next, we will actually perform this rollback within this database and fire the
// rollback event. We will also set the current transaction level to the given
// level that was passed into this method so it will be right from here out.
try {
$this->performRollBack($toLevel);
} catch (Exception $e) {
$this->handleRollBackException($e);
}
$this->transactions = $toLevel;
$this->fireConnectionEvent('rollingBack');
}
#Hyperf\Event\ListenerProvider
class ListenerProvider implements ListenerProviderInterface
{
/**
* @var ListenerData[]
*/
public $listeners = [];
/**
* @param object $event An event for which to return the relevant listeners
* @return iterable[callable] An iterable (array, iterator, or generator) of callables. Each
* callable MUST be type-compatible with $event.
*/
public function getListenersForEvent($event): iterable
{
$queue = new SplPriorityQueue();
foreach ($this->listeners as $listener) {
if ($event instanceof $listener->event) {
$queue->insert($listener->listener, $listener->priority);
}
}
return $queue;
}
public function on(string $event, callable $listener, int $priority = 1): void
{
$this->listeners[] = new ListenerData($event, $listener, $priority);
}
}
#Hyperf\Event\ListenerProviderFactory
public function __invoke(ContainerInterface $container)
{
$listenerProvider = new ListenerProvider();
// Register config listeners.
$this->registerConfig($listenerProvider, $container);
// Register annotation listeners.
$this->registerAnnotations($listenerProvider, $container);
return $listenerProvider;
}
private function registerAnnotations(ListenerProvider $provider, ContainerInterface $container): void
{
foreach (AnnotationCollector::list() as $className => $values) {
/** @var Listener $annotation */
if ($annotation = $values['_c'][Listener::class] ?? null) {
$this->register($provider, $container, $className, (int) $annotation->priority);
}
}
}
private function register(ListenerProvider $provider, ContainerInterface $container, string $listener, int $priority = 1): void
{
$instance = $container->get($listener);
if ($instance instanceof ListenerInterface) {
foreach ($instance->listen() as $event) {
$provider->on($event, [$instance, 'process'], $priority);
}
}
}
#Hyperf\Database\Model\Model
protected function bootIfNotBooted(): void {
$booted = Booted::$container[static::class] ?? false;
if (!$booted) {
Booted::$container[static::class] = true;
$this->fireModelEvent('booting');
$this->boot();
$this->fireModelEvent('booted');
}
}
protected function bootIfNotBooted(): void {
$booted = Booted::$container[static::class] ?? false;
if (!$booted) {
Booted::$container[static::class] = true;
$this->fireModelEvent('booting');
$this->boot();
$this->fireModelEvent('booted');
}
}
public function newFromBuilder($attributes = [], $connection = null) {
$model = $this->newInstance([], true);
$model->setRawAttributes((array) $attributes, true);
$model->setConnection($connection ?: $this->getConnectionName());
$model->fireModelEvent('retrieved', false);
return $model;
}
protected function performInsert(Builder $query) {
if ($event = $this->fireModelEvent('creating')) {
if ($event instanceof StoppableEventInterface && $event->isPropagationStopped()) {
return false;
}
}
// First we'll need to create a fresh query instance and touch the creation and
// update timestamps on this model, which are maintained by us for developer
// convenience. After, we will just continue saving these model instances.
if ($this->usesTimestamps()) {
$this->updateTimestamps();
}
// If the model has an incrementing key, we can use the "insertGetId" method on
// the query builder, which will give us back the final inserted ID for this
// table from the database. Not all tables have to be incrementing though.
$attributes = $this->getAttributes();
if ($this->getIncrementing()) {
$this->insertAndSetId($query, $attributes);
}
// If the table isn't incrementing we'll simply insert these attributes as they
// are. These attribute arrays must contain an "id" column previously placed
// there by the developer as the manually determined key for these models.
else {
if (empty($attributes)) {
return true;
}
$query->insert($attributes);
}
// We will go ahead and set the exists property to true, so that it is set when
// the created event is fired, just in case the developer tries to update it
// during the event. This will allow them to do so and run an update here.
$this->exists = true;
$this->wasRecentlyCreated = true;
$this->fireModelEvent('created');
return true;
}
protected function performUpdate(Builder $query) {
// If the updating event returns false, we will cancel the update operation so
// developers can hook Validation systems into their models and cancel this
// operation if the model does not pass validation. Otherwise, we update.
if ($event = $this->fireModelEvent('updating')) {
if ($event instanceof StoppableEventInterface && $event->isPropagationStopped()) {
return false;
}
}
// First we need to create a fresh query instance and touch the creation and
// update timestamp on the model which are maintained by us for developer
// convenience. Then we will just continue saving the model instances.
if ($this->usesTimestamps()) {
$this->updateTimestamps();
}
// Once we have run the update operation, we will fire the "updated" event for
// this model instance. This will allow developers to hook into these after
// models are updated, giving them a chance to do any special processing.
$dirty = $this->getDirty();
if (count($dirty) > 0) {
$this->setKeysForSaveQuery($query)->update($dirty);
$this->syncChanges();
$this->fireModelEvent('updated');
}
return true;
}
public function save(array $options = []): bool {
$this->mergeAttributesFromClassCasts();
$query = $this->newModelQuery();
// If the "saving" event returns false we'll bail out of the save and return
// false, indicating that the save failed. This provides a chance for any
// listeners to cancel save operations if validations fail or whatever.
if ($saving = $this->fireModelEvent('saving')) {
if ($saving instanceof StoppableEventInterface && $saving->isPropagationStopped()) {
return false;
}
}
// If the model already exists in the database we can just update our record
// that is already in this database using the current IDs in this "where"
// clause to only update this model. Otherwise, we'll just insert them.
if ($this->exists) {
$saved = $this->isDirty() ? $this->performUpdate($query) : true;
} else {
// If the model is brand new, we'll insert it into our database and set the
// ID attribute on the model to the value of the newly inserted row's ID
// which is typically an auto-increment value managed by the database.
$saved = $this->performInsert($query);
if (!$this->getConnectionName() && $connection = $query->getConnection()) {
$this->setConnection($connection->getName());
}
}
// If the model is successfully saved, we need to do a few more things once
// that is done. We will call the "saved" method here to run any actions
// we need to happen after a model gets successfully saved right here.
if ($saved) {
$this->finishSave($options);
}
return $saved;
}
protected function finishSave(array $options) {
$this->fireModelEvent('saved');
if ($this->isDirty() && ($options['touch'] ?? true)) {
$this->touchOwners();
}
$this->syncOriginal();
}
public function delete() {
$this->mergeAttributesFromClassCasts();
if (is_null($this->getKeyName())) {
throw new Exception('No primary key defined on model.');
}
// If the model doesn't exist, there is nothing to delete so we'll just return
// immediately and not do anything else. Otherwise, we will continue with a
// deletion process on the model, firing the proper events, and so forth.
if (!$this->exists) {
return;
}
if ($event = $this->fireModelEvent('deleting')) {
if ($event instanceof StoppableEventInterface && $event->isPropagationStopped()) {
return false;
}
}
// Here, we'll touch the owning models, verifying these timestamps get updated
// for the models. This will allow any caching to get broken on the parents
// by the timestamp. Then we will go ahead and delete the model instance.
$this->touchOwners();
$this->performDeleteOnModel();
// Once the model has been deleted, we will fire off the deleted event so that
// the developers may hook into post-delete operations. We will then return
// a boolean true as the delete is presumably successful on the database.
$this->fireModelEvent('deleted');
return true;
}
#Hyperf\Database\Model\SoftDeletes
public function restore()
{
// If the restoring event does not return false, we will proceed with this
// restore operation. Otherwise, we bail out so the developer will stop
// the restore totally. We will clear the deleted timestamp and save.
if ($event = $this->fireModelEvent('restoring')) {
if ($event instanceof StoppableEventInterface && $event->isPropagationStopped()) {
return false;
}
}
$this->{$this->getDeletedAtColumn()} = null;
// Once we have saved the model, we will fire the "restored" event so this
// developer will do anything they need to after a restore operation is
// totally finished. Then we will return the result of the save call.
$this->exists = true;
$result = $this->save();
$this->fireModelEvent('restored');
return $result;
}
可能是因为版本问题,没查到forceDeleting、forceDeleted位置。其次SoftDeletes::restore(),好像需要手动调用。
设置监听内容通过调用ListenerProviderFactory,执行通过Hyperf\Database\Connection::fireModelEvent()。