2025-06-03
// 自动加载 Command 目录下的所有命令
$commandPath = __DIR__.'/../src/Command';
$namespace = 'App\\Command\\';

foreach (new RecursiveIteratorIterator(new RecursiveDirectoryIterator($commandPath)) as $file) {
    if ($file->isFile() && $file->getExtension() === 'php') {
        $class = $namespace . str_replace(
            ['/', '.php'],
            ['\\', ''],
            substr($file->getPathname(), strlen($commandPath) + 1)
        );

        if (class_exists($class)) {
            $reflection = new ReflectionClass($class);
            if (!$reflection->isAbstract() && $reflection->isSubclassOf('Symfony\\Component\\Console\\Command\\Command')) {
                $application->add(new $class());
            }
        }
    }
}
标签: PHP
2025-05-16

https://github.com/P3TERX/GeoLite.mmdb?tab=readme-ov-file

下载GeoLite2-Country.mmdb

$ mkdir ip
$ cd ip
$ composer init
$ composer require geoip2/geoip2
$ touch index.php

<?php

include './vendor/autoload.php';
use GeoIp2\Database\Reader;

$ip = $_GET['ip'] ?? '';
if ($ip) {
    $geoip = new Reader('GeoLite2-Country.mmdb');
    $reader = $geoip->country($ip);
    print_r(json_encode($reader->jsonSerialize()));
} else {
    print_r(json_encode([]));
}
标签: PHP
2025-04-14

也可以使用此包: https://github.com/symfony/mime

function check_is_img($file) {
    $maps = [
        'jpeg' => ['image/jpeg', 'image/pjpeg'],
        'jpg' => ['image/jpeg', 'image/pjpeg'],
        'png' => ['image/png', 'image/apng', 'image/vnd.mozilla.apng'],
        'gif' => ['image/gif'],
    ];

    // get mime type.
    $image_mime = mime_content_type($file);

    // get ext.
    $ext = pathinfo($file, PATHINFO_EXTENSION);

    if (!isset($maps[$ext])) {
        return false;
    }

    $file_mine = $maps[$ext];
    return in_array($image_mime, $file_mine);
}

$file = './test_php.png';
var_dump(check_is_img($file));

 

标签: PHP
2025-04-14

定义一个类

$obj = new class() {
    public function getUser($id) {
        if ($id == 2) {
            return $this;
        }
        return null;
    }
    public function getName() {
        return 'xiukun';
    }
};

 

像以前的做法想要获取name会一层一层的判断下来

if (is_null($obj)) {
    $name = null;
} else {
    $user = $obj->getUser(5);

    if (is_null($user)) {
        $name = null;
    } else {
        $name = $user->getName();
    }
}

 

使用空运算符

$name = $obj?->getUser(1)?->getName();

 

 

标签: PHP
2025-04-14
$ composer require symfony/http-foundation

Drupal自带这个包并不需要安装

 

include_once "./vendor/autoload.php";
$header = [
    'Content-Encoding'    => 'UTF-8',
    'Content-Type'        => 'text/csv;charset=UTF-8',
    'Content-Disposition' => "attachment;filename=\"mycsv.csv\"",
];
$response = new \Symfony\Component\HttpFoundation\StreamedResponse(function() {
    $handle = fopen('php://output', 'w');
    // 这里查询你的数据库和设置csv记录.
    while (true) {
        fputcsv($handle, ['a', 'b', 'c']);
    }
    fclose($handle);
}, 200, $header);
$response->send();
exit();

 

方法2原生的PHP

header('Content-Encoding: UTF-8');
header('Content-Type: text/csv;charset=UTF-8');
header('Content-Disposition: attachment;filename="mycsv.csv"');
$handle = fopen('php://output', 'w');
// 这里查询你的数据库和设置csv记录.
while (true) {
    fputcsv($handle, ['a', 'b', 'c']);
}
fclose($handle);

 

标签: PHP
2025-04-14

被这个问题debug了一个上午. 

class boot {
    static $table;
}

class customer extends boot {
    public static function get_table() {
        static::$table = 'customer';
    }
}

class api extends boot {
    static function get_table() {
        static::$table = 'api_log_use';
        call_user_func(['customer', 'get_table']);
        var_dump(static::$table);
    }

}

api::get_table();

 

当在api类中call customer类的时候。这个$table变量会被覆盖. 最终解决办法: 在每个类中独立定义$table这个变量

class boot {
    static $table;
}

class customer extends boot {
    static $table;
    public static function get_table() {
        static::$table = 'customer';
    }
}

class api extends boot {
    static $table;
    static function get_table() {
        static::$table = 'api_log_use';
        call_user_func(['customer', 'get_table']);
        var_dump(static::$table);
    }

}

api::get_table();

 

标签: PHP
2025-04-14

比如页面中。需要向用户发邮件和写入用户访问日志等。其实这些并不需要直接表现在页面。因为会影响页面响应速度. 我们可以利用drupal terminate功能将这种功能在terminate event中实现并不需要影响页面输出

terminate功能请看index.php

<?php

/**
 * @file
 * The PHP page that serves all page requests on a Drupal installation.
 *
 * All Drupal code is released under the GNU General Public License.
 * See COPYRIGHT.txt and LICENSE.txt files in the "core" directory.
 */
use Drupal\Core\DrupalKernel;
use Symfony\Component\HttpFoundation\Request;
$autoloader = require_once 'autoload.php';

$kernel = new DrupalKernel('prod', $autoloader);

$request = Request::createFromGlobals();
$response = $kernel->handle($request);
$response->send();

$kernel->terminate($request, $response);

 

在$response->send()了以后其实页面就已经完成了,这里使用的是

fastcgi_finish_request, 这里请看我的另一篇文章。PHP提前响应页面

。下面的$kernel->terminate()这里其实是在后台悄悄完成。drupal提供了event

return [
      KernelEvents::TERMINATE => ['onKernelTerminate'],
 ];

 

功能实现

先定义一个function用来保存需要在terminate中需要调用的函数和参数

function _terminate_run($callback, $arguments = []) {
  $terminate_callbacks = &drupal_static('terminate_callbacks');
  if (!isset($terminate_callbacks)) {
    $terminate_callbacks = [];
  }
  $terminate_callbacks[] = [
    'callback' => $callback,
    'arguments' => $arguments
  ];
}

 

接着定义一个服务。用来执行event

services:
  mymodule.event_subscriber:
    class: Drupal\mymodule\EventSubscriber\AutomatedTaskSubscriber
    tags:
      - { name: event_subscriber }

 

定义这个类

<?php

declare(strict_types=1);

namespace Drupal\mymodule\EventSubscriber;

use Symfony\Component\EventDispatcher\EventSubscriberInterface;
use Symfony\Component\HttpKernel\KernelEvents;

/**
 * @todo Add description for this subscriber.
 */
final class AutomatedTaskSubscriber implements EventSubscriberInterface {


  /**
   * Kernel response event handler.
   */
  public function onKernelTerminate(): void {
    // 通过函数获取保存的callback
    $terminate_callbacks = drupal_static('terminate_callbacks');
    if ($terminate_callbacks) {
      foreach ($terminate_callbacks as $callback_infos) {
        try {
          \Drupal::logger('terminate_callbacks')->notice('terminate callback is running: <pre>' . print_r($callback_infos, true));
          call_user_func($callback_infos['callback'], $callback_infos['arguments']);
        } catch (\Exception $e) {
          \Drupal::logger('terminate_callbacks')->error('terminate callback is error: ' . $e->getMessage());
        }
      }
    }
  }

  /**
   * {@inheritdoc}
   */
  public static function getSubscribedEvents(): array {
    return [
      KernelEvents::TERMINATE => ['onKernelTerminate'],
    ];
  }

}

 

测试

 

function write_file_fuc($time) {
  file_put_contents('write_file_fuc.txt', $time);
}


/**
 * Returns responses for test_code routes.
 */
class TestCodeController extends ControllerBase {



  public function writefile($time) {
    sleep(10);
    file_put_contents('time.txt', $time);
  }

  /**
   * Builds the response.
   */
  public function build() {

    _terminate_run([$this, 'writefile'], [time()]);
    _terminate_run('write_file_fuc', [time()]);
    $build['content'] = [
      '#type' => 'item',
      '#markup' => $this->t('It works!'),
    ];

    return $build;
  }

}

 

这里可以看到。页面响应速度会很快而且不会影响写入一个文件的功能. 就算写入文件需要等10秒也是后端执行

我们通常这里可以用来做一些不影响页面响应但是又要很久的事情。比如发邮件。比如写日志

 

标签: PHP
2025-04-14

应用场景

1. 用户获取接口. 获取接口了以后还需要写入日志. 但是写入日志后面这段用户可以不用知道。可以先把数据响应给用户
2. 我们提供一个通知接口. 我们在收到接口了以后直接可以告诉用户收到了。而不是处理一大堆逻辑成功了再通知用户我们收到了

php实现

<?php

function delayOutput($output) {
    $is_fastcgi = function_exists("fastcgi_finish_request");
    if (!$is_fastcgi) {
        ob_end_clean();
        ob_start();
        header("Connection: close\r\n");
        header("Content-Encoding: none\r\n");
    }

    echo $output;

    if(!$is_fastcgi){
        $size = ob_get_length();
        header("Content-Length: ". $size . "\r\n");
        ob_end_flush();
        flush();
        header("Test-Header: fastcgi_finish_request\r\n");
        ignore_user_abort(true);
    }else{
        fastcgi_finish_request();
    }
}

delayOutput('hello world');
sleep(5);
file_put_contents('delay.txt', 'delay');

在Drupal接口中如何用这个东西

namespace Drupal\mymodule\Plugin\rest\resource;

use Drupal\rest\Plugin\ResourceBase;
use Symfony\Component\HttpFoundation\JsonResponse;

/**
 * Represents TestApi records as resources.
 *
 * @RestResource (
 *   id = "rest_output_testapi",
 *   label = @Translation("TestApi"),
 *   uri_paths = {
 *     "canonical" = "/api/testapi"
 *   }
 * )
 *
 * For entities, it is recommended to use REST resource plugin provided by
 * Drupal core.
 * @see \Drupal\rest\Plugin\rest\resource\EntityResource
 */
final class TestapiResource extends ResourceBase {


  /**
   * Responds to GET requests.
   */
  public function get() {
    $data = [
      'userid' => 1,
      'username' => 'xiukunabcd'
    ];
    $response = new JsonResponse($data);
    // 因为这里的send已经实现了fastcgi_finish_request. 所以不需要过多的逻辑
    $response->send();

    //先输出内容. 后面用于写入日志啊这种不需要响应给用户的处理
    sleep(20);
    file_put_contents('test.txt', 'test');
    
  }
}

 

标签: PHP
2025-04-14

需求.

  1. 用户登陆成功根据用户的专业给用户推送产品.
  2. 用户登陆成功给用户发送一封邮件
  3. 用户登陆成功判断后一次登陆时间如果超过1个月告诉通知营销部门
  4. 用户登陆成功如果角色是超级管理员自动跳转到 /admin/contents 页面
  5. 用户登陆成功如果角色是网站管理员自动跳转到 /manage页面

 

先定义一个模块来完成我们的需求. 模块名为.

普通的做法

use Drupal\user\UserInterface;

/**
 * Implements hook_user_login().
 *
 * @param UserInterface $account
 * @throws \Drupal\Core\Entity\EntityMalformedException
 */
function observer_user_login(UserInterface $account) {

  // #############  用户登陆成功根据用户的专业给用户推送产品. ########## //
  switch ($account->get('field_profession')->value) {
    case 'A':
      //一大堆逻辑
      break;
    case 'B':
      // 一大堆逻辑
      break;
    case 'C':
      // 一大堆逻辑
      break;
    default:
      // 一大堆逻辑
      break;
  }

  // #############  用户登陆成功给用户发送一封邮件. ########## //
  // 发送邮件逻辑

  // #############  用户登陆成功判断后一次登陆时间如果超过1个月告诉通知营销部门. ########## //
  if (time() - $account->get('access')->value > 3000) {
     // 通知营销部门逻辑
  }

  // #############  用户登陆成功如果角色是超级管理员自动跳转到 /admin/contents 页面. ########## //
  if ($account->hasRole('administrator')) {
    // 跳转逻辑
  }

  // #############  用户登陆成功如果角色是网站管理员自动跳转到 /manage页面. ########## //
  if ($account->hasRole('webmaster')) {
    // 跳转逻辑
  }
}

这里的代码越后面会越乱,如果到时再增加一个需求登陆成功要做某事。可能又是一大堆逻辑.

现在改用普通的PHP观察者模式分离代码

PHP观察者模式

定义: src/Observer.php

<?php

namespace Drupal\observer;

use Drupal\user\UserInterface;

/**
 * 创建主题用来注册所有的观察者.
 *
 * Class Observer
 */
class Observer implements \SplSubject {

  private array $observers = [];

  private UserInterface $account;

  public function setAccount(UserInterface $account) {
    $this->account = $account;
  }

  public function getAccount() {
    return $this->account;
  }

  /**
   * 注册观察者.
   *
   * @param \SplObserver $observer
   */
  public function attach(\SplObserver $observer):void {
    $this->observers[] = $observer;
  }

  /**
   * 移掉观察者.
   *
   * @param \SplObserver $observer
   */
  public function detach(\SplObserver $observer):void {
    if ($key = array_search($observer,$this->observers, true)){
      unset($this->observers[$key]);
    }
  }

  /**
   * 通知观察者.
   */
  public function notify():void {
    foreach ($this->observers as $value) {
      $value->update($this);
    }
  }
}

 

分别定义功能代码:
src/Observers/Notice.php
src/Observers/NoticeMarkting.php
src/Observers/PushProduct.php
src/Observers/RedAdmin.php
src/Observers/RedWebmaster.php
代码都类似如下。只是类名不一样.

 
 
 
<?php
namespace Drupal\observer\Observers;

use Drupal\observer\Observer;
use SplSubject;

class Notice implements \SplObserver {

  public function update(Observer|SplSubject $subject):void {
    $account = $subject->getAccount();
    \Drupal::logger('observer')->notice('用户登陆成功给用户发送一封邮件: ' . $account->label());
  }
}

 

然后修改hook

<?php

use Drupal\observer\Observers\Notice;
use Drupal\observer\Observers\NoticeMarkting;
use Drupal\observer\Observers\PushProduct;
use Drupal\observer\Observers\RedAdmin;
use Drupal\observer\Observers\RedWebmaster;

use Drupal\user\UserInterface;
use Drupal\observer\Observer;

/**
 * Implements hook_user_login().
 *
 * @param UserInterface $account
 */
function observer_user_login(UserInterface $account) {
  $observer = new Observer();
  $observer->setAccount($account);

  $observer->attach(new Notice());
  $observer->attach(new NoticeMarkting());
  $observer->attach(new PushProduct());
  $observer->attach(new RedAdmin());
  $observer->attach(new RedWebmaster());

  $observer->notify();
}

 
Drupal的event和hook就很类似观察者模式. 现在可以使用event来实现这段代码
 


Event实现

这里定义一个新的模块: obevent

先自定义一个用户登陆的事件: src/Event/UserLoginEvent.php

<?php
namespace Drupal\obevent\Event;

use Drupal\Component\EventDispatcher\Event;
use Drupal\user\UserInterface;

class UserLoginEvent extends Event {

  const EVENT_NAME = 'custom_event_user_login';

  /**
   * @var \Drupal\user\UserInterface
   */
  public UserInterface $account;

  public function getAccount() {
    return $this->account;
  }

  public function setAccount($account) {
    $this->account = $account;
  }

  public function __construct(UserInterface $account) {
    $this->setAccount($account);
  }
}

 

定义功能实现的服务: obevent.services.yml

services:
  obevent.user_login_notice_event_subscriber:
    class: Drupal\obevent\EventSubscriber\LoginNoticeSubscriber
    tags:
      - { name: event_subscriber }
  obevent.user_login_notice_markting_event_subscriber:
    class: Drupal\obevent\EventSubscriber\LoginNoticeMarktingSubscriber
    tags:
      - { name: event_subscriber }
  obevent.user_login_push_product_event_subscriber:
    class: Drupal\obevent\EventSubscriber\LoginPushProductSubscriber
    tags:
      - { name: event_subscriber }

  obevent.user_login_red_admin_subscriber:
    class: Drupal\obevent\EventSubscriber\LoginRedAdminSubscriber
    tags:
      - { name: event_subscriber }

  obevent.user_login_red_webmater_subscriber:
    class: Drupal\obevent\EventSubscriber\LoginRedWebmasterSubscriber
    tags:
      - { name: event_subscriber }
 

定义功能实现代码:

src/EventSubscriber/LoginNoticeMarktingSubscriber.php
src/EventSubscriber/LoginNoticeSubscriber.php
src/EventSubscriber/LoginPushProductSubscriber.php
src/EventSubscriber/LoginRedAdminSubscriber.php
src/EventSubscriber/LoginRedWebmasterSubscriber.php

功能类似下面这样. 只是类名不一样

<?php

declare(strict_types=1);

namespace Drupal\obevent\EventSubscriber;

use Drupal\obevent\Event\UserLoginEvent;
use Symfony\Component\EventDispatcher\EventSubscriberInterface;

final class LoginNoticeSubscriber implements EventSubscriberInterface {

  /**
   * Kernel request event handler.
   */
  public function onUserLogin(UserLoginEvent $event): void {
    $account = $event->getAccount();
    \Drupal::logger('observer')->notice('用户登陆成功给用户发送一封邮件: ' . $account->label());
  }

  /**
   * {@inheritdoc}
   */
  public static function getSubscribedEvents(): array {
    return [
      UserLoginEvent::EVENT_NAME => ['onUserLogin'],
    ];
  }

}
 
 
修改obevent.module文件.
<?php

use Drupal\obevent\Event\UserLoginEvent;
use Drupal\user\UserInterface;

/**
 * Implements hook_user_login().
 *
 * @param UserInterface $account
 */
function obevent_user_login(UserInterface $account) {
  $event = new UserLoginEvent($account);
  \Drupal::service('event_dispatcher')->dispatch($event, UserLoginEvent::EVENT_NAME);
}
标签: PHP 设计模式
2025-04-14

在以前类初始化赋值是需要这样的

class MyClass {
  private $a;
  private $b;
  public function __construct(string $a, string $b) {
    $this->a = $a;
    $this->b = $b;

    var_dump($this->a);
  }
}

PHP8

class Abcd {

  public function __construct(private string $a, public private $b) {
    var_dump($this->a);
  }

}

或者如下:

class Abcd {

  public function __construct(public readonly string $a, public readonly string $b) {
    var_dump($this->a);
  }

}

$abcd = new Abcd('a', 'b');
var_dump($abcd->a);

原文: https://www.php.net/manual/en/language.oop5.decon.php#language.oop5.decon.constructor.promotion

 

标签: PHP