hyperf 二十三 分页

教程:Hyperf

参考:

hyperf 十、分页-CSDN博客

illuminate/database 使用 一_illuminate db类-CSDN博客

hyperf 二十二 数据库 模型关系-CSDN博客

一 分页查询

1.1 原理

根据文档,可以使用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类。

1.2 测试

$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)
}

1.3 源码

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'));
    }

二 模型事件

2.1 系统运行事件

其实就是系统运行位置的钩子,设置监听时使用。

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中设置。

2.2 模型事件

钩子函数

事件名 触发实际 是否阻断 备注
booting 模型首次加载前 进程生命周期中只会触发一次
booted 模型首次加载后 进程生命周期中只会触发一次
retrieved 填充数据后 每当模型从 DB 或缓存查询出来后触发
creating 数据创建时
created 数据创建后
updating 数据更新时
updated 数据更新后
saving 数据创建或更新时
saved 数据创建或更新后
restoring 软删除数据恢复时
restored 软删除数据恢复后
deleting 数据删除时
deleted 数据删除后
forceDeleting 数据强制删除时
forceDeleted 数据强制删除后

 测试参考:hyperf 二十二 数据库 模型关系-CSDN博客中测试中设置的自定义多态关系的监听。

2.3 源码

2.3.1 系统事件

#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);
            }
        }
    }

 2.3.2 模型事件

#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()。

你可能感兴趣的:(php,php,hyperf)