🦊
cakephp2リプレイスUploadPack(ファイルアップロードプラグイン)の罠
cakephp2をリプレイスしていてハマった。
ハマったというより脱力した。
どうして...どうしてcakephp2で対応止まっているの...
ということでcomponentとhelper、gdを用いてみたような機能を作成してみる
プラグインにする気はない。(長いしメンテしたくないので)
cakephp5.0.10
php8.3
apache2
ubuntu24.04
汎用的ファイルの作成
src/Controller/Component/BaseTrait.php
<?php
declare(strict_types=1);
namespace App\Controller\Component;
use Cake\ORM\TableRegistry;
trait BaseTrait {
protected function controller() {
return $this->_registry->getController();
}
protected function request() {
return $this->controller()->getRequest();
}
protected function response() {
return $this->controller()->getResponse();
}
protected function session() {
return $this->controller()->getRequest()->getSession();
}
protected function getTable($table) {
return TableRegistry::getTableLocator()->get($table);
}
}
src/Controller/AppController.php
<?php
declare(strict_types=1);
/**
* CakePHP(tm) : Rapid Development Framework (https://cakephp.org)
* Copyright (c) Cake Software Foundation, Inc. (https://cakefoundation.org)
*
* Licensed under The MIT License
* For full copyright and license information, please see the LICENSE.txt
* Redistributions of files must retain the above copyright notice.
*
* @copyright Copyright (c) Cake Software Foundation, Inc. (https://cakefoundation.org)
* @link https://cakephp.org CakePHP(tm) Project
* @since 0.2.9
* @license https://opensource.org/licenses/mit-license.php MIT License
*/
namespace App\Controller;
use Cake\Controller\Controller;
use Cake\Datasource\ConnectionManager;
/**
* Application Controller
*
* Add your application-wide methods in the class below, your controllers
* will inherit them.
*
* @link https://book.cakephp.org/5/en/controllers.html#the-app-controller
* @property \Authentication\Controller\Component\AuthenticationComponent $Authentication
* @property \App\Controller\Component\CommonComponent $Common
*/
class AppController extends Controller
{
/**
* Initialization hook method.
*
* Use this method to add common initialization code like loading components.
*
* e.g. `$this->loadComponent('FormProtection');`
*
* @return \Cake\Http\Response|null|void
*/
public function initialize(): void
{
parent::initialize();
$this->loadComponent('Flash');
/*
* Enable the following component for recommended CakePHP form protection settings.
* see https://book.cakephp.org/5/en/controllers/components/form-protection.html
*/
//$this->loadComponent('FormProtection');
$this->loadComponent('Authentication.Authentication');
$this->loadComponent('ImageUP');
}
/**
* session function
*
* セッション
*
* @return \Cake\Http\Session
*/
public function session()
{
return $this->getRequest()->getSession();
}
/**
* connection function
*
* トランザクション用
*
* @return \Cake\Datasource\ConnectionInterface
*/
public function connection()
{
return ConnectionManager::get('default');
}
/**
* getLoginUser function
*
* ログインしているユーザーの情報を取得する
*
* @return \Authentication\IdentityInterface|null
*/
public function getLoginUser($key)
{
$identity = $this->Authentication->getIdentity();
if ($key && $identity) {
return $identity[$key];
}
return $identity;
}
}
src/Controller/Component/ImageUPComponent.php
<?php
declare(strict_types=1);
namespace App\Controller\Component;
use Cake\Controller\Component;
use Cake\Controller\ComponentRegistry;
use App\Controller\Component\BaseTrait;
use Exception;
/**
* Common component
*/
class ImageUPComponent extends Component
{
use BaseTrait;
// 画像保存で生成される画像リスト
protected array $createImageSet = [
'_thumb.' =>
[
'max_width' => 100,
'max_height' => 100
]
];
/**
* initialize function
*
* @param array $config
* @return \Cake\Http\Response|null|void
*/
public function initialize(array $config): void
{
parent::initialize($config);
}
/**
* upload function
*
* 画像のアップロード
*
* @param entity $imageEntity 対象テーブルのエンティティ
* @param mixed $imageFile 画像ファイル
* @return bool
*/
public function upload($imageEntity, $imageFile):bool
{
// 元画像の名前を取得
$name = $imageFile->getClientFilename();
// 元画像の名前(拡張子抜き)
$this->extension = pathinfo($name, PATHINFO_EXTENSION);
// 元画像の拡張子
$this->filename = pathinfo($name, PATHINFO_FILENAME);
// inputされたファイルのpath
$this->image_uri = $imageFile->getStream()->getMetadata('uri');
// 元動画のサイズ
$this->image_size_width = getimagesize($this->image_uri)[0];
$this->image_size_height = getimagesize($this->image_uri)[1];
// 元画像の縦横比
$this->aspect = $this->image_size_height / $this->image_size_width;
$this->model = strtolower($imageEntity->modelName);
$dirPath = WWW_ROOT . 'uploads' . DS . $this->model . DS . $imageEntity['id'];
// ディレクトリが存在するかチェック
if (!file_exists($dirPath)) {
// フォルダを作成(0775はフォルダのパーミッション)
mkdir($dirPath, 0775, true);
}
// 元画像をwebroot内に保存する
$image_name = $this->filename . '_original.' . $this->extension;
$this->original_path = WWW_ROOT . 'uploads' . DS . $this->model . DS . $imageEntity['id'] . DS . $image_name;
if ($image_name) {
$imageFile->moveTo($this->original_path);
// サムネを作る
// 元画像をGDに変換する
$original_image = $this->loadImageToGD($this->original_path);
foreach ($this->createImageSet as $key => $val) {
$create_thumb = $this->createImage($original_image, $imageEntity['id'], $key, (int)$val['max_width'], (int)$val['max_height']);
(!$create_thumb) ? throw new Exception('保存失敗') : '';
}
// GDのメモリを解放する
imagedestroy($original_image);
return true;
} else {
return false;
}
}
/**
* createImage function
*
* 元画像を元にサイズ変更した画像を作成
*
* @param mixed $original_image_gd 元動画のGD
* @param int $id
* @param string $style _thumb.
* @param int $max_width 作成する画像の幅
* @param int $max_height 作成する画像の高さ
* @return bool
*/
private function createImage($original_image_gd, $id, $style, $max_width, $max_height):bool
{
$image_name = $this->filename . $style . $this->extension;
$save_path = WWW_ROOT . 'uploads' . DS . $this->model . DS . $id . DS . $image_name;
// 新しい画像をGDで作成
$new_image = imagecreatetruecolor($max_width, $max_height);
// 背景を白にする
$white = imagecolorallocate($new_image, 255, 255, 255);
imagefill($new_image, 0, 0, $white);
// 元画像の縦横比を合わせたwidthとheightを計算する
$new_size = $this->calcAspect($max_width, $max_height);
// 中央に画像を配置するための座標を計算
$x_offset = ($max_width - $new_size['width']) / 2;
$y_offset = ($max_height - $new_size['height']) / 2;
// 画像をコピーしてリサイズ
imagecopyresampled($new_image, $original_image_gd, (int)$x_offset, (int)$y_offset, 0, 0, $new_size['width'], $new_size['height'], $this->image_size_width, $this->image_size_height);
// 拡張子に合わせて保存する
switch ($this->extension) {
case 'jpg':
case 'jpeg':
imagejpeg($new_image, $save_path);
break;
case 'png':
imagepng($new_image, $save_path);
break;
case 'webp':
imagewebp($new_image, $save_path);
break;
}
// メモリを解放
imagedestroy($new_image);
return true;
}
/**
* calcAspect function
*
* 元画像の縦横比と合わせた幅と高さを返す
*
* @param int $max_width 幅
* @param int $max_height 高さ
* @return array
*/
private function calcAspect($max_width, $max_height):array
{
// 幅を基準に高さを計算
$new_width = $max_width;
$new_height = $new_width * $this->aspect;
// 高さが最大高さを超えた場合、高さを基準に幅を再計算
if ($new_height > $max_height) {
$new_height = $max_height;
$new_width = $new_height / $this->aspect;
}
return [
'width' => (int)$new_width,
'height' => (int)$new_height
];
}
/**
* loadImageToGD function
*
* 画像をgdに変換する
*
* @param string $path フルパス
* @return resource|\GdImage|false
*/
private function loadImageToGD($path)
{
switch ($this->extension) {
case 'jpg':
case 'jpeg':
$original_image = imagecreatefromjpeg($path);
break;
case 'png':
$original_image = imagecreatefrompng($path);
break;
case 'webp':
$original_image = imagecreatefromwebp($path);
break;
}
return $original_image;
}
/**
* delete function
*
* @param int $id
* @param string $image filename
* @return \Cake\Http\Response|null|void
*/
public function delete($entity,$id, $image)
{
$filename = pathinfo($image, PATHINFO_FILENAME);
$path = WWW_ROOT . 'uploads' . DS . strtolower($entity->modelName) . DS . $id . DS . $filename . '*';
$files = glob($path);
foreach ($files as $file) {
if (file_exists($file)) {
// ファイルを削除
unlink($file);
}
}
return;
}
/**
* uploadFile function
*
* 画像ではなくオリジナルファイルしか保存しないものをアップロードする
*
* @param [type] $imageEntity
* @param [type] $imageFile
* @return boolean
*/
public function uploadFile($imageEntity, $imageFile):bool
{
// 元画像の名前を取得
$name = $imageFile->getClientFilename();
// 元画像の名前(拡張子抜き)
$this->extension = pathinfo($name, PATHINFO_EXTENSION);
// 元画像の拡張子
$this->filename = pathinfo($name, PATHINFO_FILENAME);
$this->model = strtolower($imageEntity->modelName);
$dirPath = WWW_ROOT . 'uploads' . DS . $this->model . DS . $imageEntity['id'];
// ディレクトリが存在するかチェック
if (!file_exists($dirPath)) {
// フォルダを作成(0775はフォルダのパーミッション)
mkdir($dirPath, 0775, true);
}
// 元画像をwebroot内に保存する
$image_name = $this->filename . '_original.' . $this->extension;
$this->original_path = WWW_ROOT . 'uploads' . DS . $this->model . DS . $imageEntity['id'] . DS . $image_name;
if ($image_name) {
$imageFile->moveTo($this->original_path);
return true;
} else {
return false;
}
}
}
src/View/Helper/CustomLinkHelper.php
<?php
declare(strict_types=1);
namespace App\View\Helper;
use Cake\View\Helper;
use Cake\View\View;
/**
* CustomLink helper
*
* @property \Cake\View\Helper\HtmlHelper $Html
* @property \Cake\View\Helper\FormHelper $Form
* @property \Cake\View\Helper\UrlHelper $Url
*/
class CustomLinkHelper extends Helper
{
/**
* Default configuration.
*
* @var array<string, mixed>
*/
protected array $_defaultConfig = [];
protected array $helpers = ['Html', 'Form', 'Url'];
/**
* getOriginal function
*
* オリジナル用のリンクを返す
*
* @param entity $entity
* @param string $filename 画像ファイル名(拡張子付き)
* @param string $class 付与したいclassがあれば
* @param string $wight 付与したいwightがあれば初期値は100
* @return void
*/
public function getOriginal($entity, $filename, $class = '', $width = '', $height = '')
{
$extension = pathinfo($filename, PATHINFO_EXTENSION);
$filename = pathinfo($filename, PATHINFO_FILENAME);
$path = '/uploads/' . strtolower($entity->modelName) . '/' . $entity->id . '/' . $filename . '_original.' . $extension;
return $this->Html->image($path, ['class' => $class, 'width' => $width, 'height' => $height, 'alt' => $filename]);
}
/**
* getOriginalUrl function
*
* オリジナル用のURLを返す
*
* @param entity $entity
* @param string $filename 画像ファイル名(拡張子付き)
* @return void
*/
public function getOriginalUrl($entity, $filename)
{
$extension = pathinfo($filename, PATHINFO_EXTENSION);
$filename = pathinfo($filename, PATHINFO_FILENAME);
$path = '/uploads/' . strtolower($entity->modelName) . '/' . $entity->id . '/' . $filename . '_original.' . $extension;
return $this->Url->build($path);
}
/**
* getThumbnail function
*
* サムネイル用のリンクを返す
*
* @param mixed $entity
* @param string $filename 画像ファイル名(拡張子付き)
* @param string $class 付与したいclassがあれば
* @param string $wight 付与したいwightがあれば初期値は100
* @return void
*/
public function getThumbnail($entity, $filename, $class = '', $width = '100', $height = '')
{
$extension = pathinfo($filename, PATHINFO_EXTENSION);
$filename = pathinfo($filename, PATHINFO_FILENAME);
$path = '/uploads/'. strtolower($entity->modelName) . '/' . $entity->id . '/' . $filename . '_thumb.' . $extension;
return $this->Html->image($path, ['class' => $class, 'width' => $width, 'height' => $height,'alt' => $filename]);
}
}
<?php
namespace App\Model\Entity;
trait BaseTrait
{
public function _getModelName()
{
return $this->getSource();
}
public function mergeVirtualProperties() {
// 仮想プロパティをマージする処理
$this->_virtual = array_merge($this->_virtual, $this->getTraitVirtual());
}
}
画像のfile_nameが保存されているentityでBaseTraitを有効にする
src/Model/Entity/Image.php
// 略
class Image extends Entity
{
use BaseTrait;
// 略
呼び出し方
image画像として表示する
第一引数:エンティティ ($this->Models->get(id)で出てくるようなやつ)
第二引数:登録してある画像名
第三引数:追加したいクラスがあれば
第四引数:width
第五引数:height
<?= $this->CustomLink->getOriginal($image, $image['image_file_name']) ?>
<?= $this->CustomLink->getThumbnail($image, $image['image_file_name']) ?>
<a href="<?= $this->CustomLink->getOriginalUrl($image, $image['image_file_name']) ?>" target="_blank"></a>
画像を保存する
public function add()
{
$errors = [];
$uploadFile = [];
$uploadImage = [];
$images = $this->fetchTable('Images')->newEmptyEntity();
if ($this->request->is('post')) {
$input = $this->getRequest()->getData();
// submitされたすべてのファイルを取得
$pictures = $this->request->getUploadedFiles();
// 画像(サムネとオリジナルを保存)
foreach ($pictures['image'] as $key => $picture) {
if ($picture->getClientFilename() === '') {
continue;
}
$uploadImage[$key] = $picture;
$input[$key . '_file_name'] = $picture->getClientFilename();
}
// ファイル
foreach ($pictures['file'] as $key => $file) {
if ($file->getClientFilename() === '') {
continue;
}
$uploadFile[$key] = $file;
$input[$key . '_file_name'] = $file->getClientFilename();
}
$image = $this->fetchTable('Images')->patchEntity($images, $input);
if ($image->hasErrors()) {
$errors = $image->getErrors();
} else {
try {
$this->connection()->transactional(function () use ($uploadImage, $uploadFile, $image) {
if ($this->fetchTable('Images')->save($image)) {
// 画像(サムネとオリジナルを保存)
if (!empty($uploadImage)) {
foreach ($uploadImage as $save_image) {
if (!$this->ImageUP->upload($image, $save_image)) {
throw new Exception('画像登録失敗');
};
}
}
// ファイル
if (!empty($uploadFile)) {
foreach ($uploadFile as $file) {
if (!$this->ImageUP->uploadFile($image, $file)) {
throw new Exception('ファイル登録失敗');
};
}
}
} else {
throw new Exception('images登録失敗');
}
});
return $this->redirect(['action' => 'addComplete']);
} catch (Exception $e) {
$this->log('images登録失敗' . $e->getMessage());
}
}
}
$this->set(compact('image,'errors'));
}
あんまり綺麗に描けてないな〜
Discussion