This commit is contained in:
Cassandre Cantet 2017-10-04 00:11:25 +02:00
commit 95885c790e
12 changed files with 539 additions and 0 deletions

27
composer.json Executable file
View File

@ -0,0 +1,27 @@
{
"name": "meoran/images",
"description": "Gestion des images en base de données",
"type": "library",
"license": "MIT",
"authors": [
{
"name": "Cassandre Cantet",
"email": "pro@cassandrecantet.fr"
}
],
"require": {
"php": ">=7.0.0",
"laravel/lumen-framework": "5.4.*",
"symfony/process" : "3.*",
"intervention/image": "^2.4",
"intervention/imagecache": "^2.3",
"spatie/laravel-image-optimizer": "^1.1"
},
"autoload": {
"psr-4": {
"Meoran\\Images\\": "src/"
}
},
"prefer-stable": true,
"minimum-stability": "dev"
}

15
config/image.php Executable file
View File

@ -0,0 +1,15 @@
<?php
return [
'route' => 'images',
'path' => storage_path('images'),
'templates' => array(
'small' => \Meoran\Images\Templates\Small::class,
'medium' => \Meoran\Images\Templates\Medium::class,
'large' => \Meoran\Images\Templates\Large::class,
),
'lifetime' => 10,
'cache' => [
'path' => storage_path('app')
]
];

View File

@ -0,0 +1,34 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
class CreateImagesTable extends Migration
{
/**
* Run the migrations.
*
* @return void
*/
public function up()
{
Schema::create('images', function (Blueprint $table) {
$table->integer('id', true);
$table->string('filename');
$table->dateTime('created_at');
$table->dateTime('updated_at');
$table->unique('filename');
});
}
/**
* Reverse the migrations.
*
* @return void
*/
public function down()
{
Schema::drop('images');
}
}

View File

@ -0,0 +1,38 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
class CreateAssociateImages extends Migration
{
/**
* Run the migrations.
*
* @return void
*/
public function up()
{
Schema::create('associate_images', function (Blueprint $table) {
$table->integer('id', true);
$table->integer('image_id');
$table->string('relation_type');
$table->integer('relation_id');
$table->integer('position')->nullable();
$table->foreign('image_id')->references('id')->on('images');
});
}
/**
* Reverse the migrations.
*
* @return void
*/
public function down()
{
Schema::drop('associate_images');
}
}

View File

@ -0,0 +1,8 @@
<?php
namespace Meoran\Images\Exception;
class InvalidContent extends \Exception
{
}

View File

@ -0,0 +1,132 @@
<?php
namespace Meoran\Images\Http\Controllers;
use Closure;
use Illuminate\Http\Request;
use Laravel\Lumen\Routing\Controller as BaseController;
use Meoran\Images\Model\Image;
class ImagesController extends BaseController
{
public function get($template, $filename)
{
switch (strtolower($template)) {
case 'original':
return $this->getOriginal($filename);
case 'download':
return $this->getDownload($filename);
default:
return $this->getImage($template, $filename);
}
}
public function upload(Request $request)
{
$this->validate($request, [
'image' => 'required|file|image'
]);
$image = new Image(['content' => $request->input('image')]);
$image->save();
return response()->json($image);
}
private function getOriginal($filename)
{
$path = $this->getImagePathOrAbort($filename);
return $this->buildResponse(file_get_contents($path));
}
private function getDownload($filename)
{
$response = $this->getOriginal($filename);
return $response->header(
'Content-Disposition',
'attachment; filename=' . $filename
);
}
private function getImagePathOrAbort($filename)
{
$path = Image::getAbsolutePath($filename);
if (is_file($path)) {
return $path;
}
abort(404);
}
public function getImage($template, $filename)
{
$template = $this->getTemplate($template);
$path = $this->getImagePathOrAbort($filename);
$lifetime = config('image.lifetime');
if (empty($lifetime)) {
$content = $this->applyTemplate($template,$path)->encode();
} else {
$content = app('image')->cache(function ($image) use ($template, $path) {
$this->applyTemplate($template,$path,$image);
return $image;
}, $lifetime);
}
return $this->buildResponse($content);
}
private function applyTemplate($template, $path, $image = null)
{
if (empty($image)) {
$image = app('image');
}
if ($template instanceof Closure) {
// build from closure callback template
return $template($image->make($path));
} else {
// build from filter template
return $image->make($path)->filter($template);
}
}
/**
* Returns corresponding template object from given template name
*
* @param string $template
* @return mixed
*/
private function getTemplate($template)
{
$template = config("image.templates.{$template}");
switch (true) {
// closure template found
case is_callable($template):
return $template;
// filter template found
case class_exists($template):
return new $template;
default:
// template not found
abort(404);
break;
}
}
private function buildResponse($content)
{
// define mime type
$mime = finfo_buffer(finfo_open(FILEINFO_MIME_TYPE), $content);
// return http response
return response($content, 200, array(
'Content-Type' => $mime,
'Cache-Control' => 'max-age=' . (config('image.lifetime') * 60) . ', public',
'Etag' => md5($content)
));
}
}

174
src/Model/Image.php Executable file
View File

@ -0,0 +1,174 @@
<?php
namespace Meoran\Images\Model;
use Illuminate\Database\Eloquent\Model;
use Intervention\Image\Exception\NotReadableException;
use Meoran\Images\Exception\InvalidContent;
use Spatie\ImageOptimizer\OptimizerChain;
use \Intervention\Image\Image as InterventionImage;
/**
* Class Image
* @property Image $content
* @package App\Model
*/
class Image extends Model
{
protected $_content;
protected $table = 'images';
public $fillable = [
'filename',
'content',
'position',
'created',
'updated'
];
protected $dates = ['created_at', 'updated_at'];
protected $appends = ['url'];
protected $hidden = [];
public function getUrlAttribute()
{
return app('url')->to('/' . config('image.route') . '/original/' . $this->filename);
}
protected static function boot()
{
parent::boot();
static::creating(function (Image $model) {
if (empty($model->content)) {
throw new InvalidContent("Content must be defined to save image");
}
if (!($model->content instanceof InterventionImage)) {
throw new InvalidContent("Content must be an instance of Intervention\Image");
}
$model->savePicture();
});
static::updating(function (Image $model) {
$model->savePicture();
});
static::deleted(function (Image $model) {
$model->deletePicture();
});
}
public function getPath()
{
if (empty($this->filename)) {
return null;
}
return self::getAbsolutePath($this->filename);
}
static function getAbsolutePath($filename)
{
$basePath = config('image.path');
$parts = array_slice(str_split(mb_strtolower(str_slug($filename, '')), 2), 0, 2);
$path = $basePath . '/' . implode('/', $parts) . '/' . $filename;
return $path;
}
static function generateRandomFilename()
{
return mb_strtolower(str_random(60));
}
static function sanitizeFilename($filename)
{
return str_slug($filename, '-');
}
public function setFilenameAttribute($value)
{
$pattern = '/[^a-z_\-\.0-9]/i';
if (preg_match($pattern, $value)) {
throw new \InvalidArgumentException("Invalid filename. Must be only composed only with a-z, A-Z, 0-9 and dot minus underscore");
}
$this->attributes['filename'] = $value;
}
public function fileExist() {
return is_file($this->getPath());
}
public function generateFilename($force = false)
{
if ($this->filename && !$force) {
return;
}
$this->filename = self::generateRandomFilename();
}
public function setContentAttribute($content)
{
$this->_content = app('image')->make($content);
}
public function getContentAttribute($value)
{
if (empty($this->_content)) {
try {
$this->_content = app('image')->make($this->getPath());
} catch (NotReadableException $e) {
return null;
}
}
return $this->_content;
}
protected function savePicture()
{
if (empty($this->content)) {
return true;
}
$this->generateFilename();
return $this->saveContent();
}
protected function saveContent()
{
if (empty($this->content)) {
throw new \InvalidArgumentException("Content is Empty");
}
$path = $this->getPath();
$dir = dirname($path);
if (!is_dir($dir)) {
mkdir($dir, 0775, true);
}
$res = $this->content->save($path);
app(OptimizerChain::class)->optimize($path);
return $res;
}
protected function deletePicture()
{
$path = $this->getPath();
if (is_file($path)) {
return unlink($path);
}
return true;
}
public function toArray()
{
$attributes = parent::toArray();
if (isset($attributes['pivot']) && array_key_exists('position', $attributes['pivot'])) {
$attributes['position'] = $attributes['pivot']['position'];
}
unset($attributes['pivot']);
return $attributes;
}
}

View File

@ -0,0 +1,43 @@
<?php
namespace Meoran\Images\Providers;
use Illuminate\Support\ServiceProvider;
use Intervention\Image\ImageServiceProvider;
use Spatie\LaravelImageOptimizer\ImageOptimizerServiceProvider;
class ImagesServiceProvider extends ServiceProvider
{
public function boot()
{
require_once __DIR__ . '/../functions.php';
$this->loadMigrationsFrom(__DIR__ . '/../../database/migrations');
$this->mergeConfigFrom(__DIR__ . '/../../config/image.php', 'image');
$this->app->register(ImageServiceProvider::class);
$this->app->register(ImageOptimizerServiceProvider::class);
$this->loadRoutes();
if ($this->app->runningInConsole()) {
$this->publishes([
// __DIR__ . '/../../config/synchronize.php' => base_path('config/synchronize.php')
]);
}
}
public function loadRoutes()
{
$this->app->post('images/upload', [
'as' => 'uploadImage', 'uses' => '\Meoran\Images\Http\Controllers\ImagesController@upload'
]);
$this->app->get('images/{template}/{filename}', [
'as' => 'getPicture', 'uses' => '\Meoran\Images\Http\Controllers\ImagesController@get'
]);
}
}

18
src/Templates/Large.php Executable file
View File

@ -0,0 +1,18 @@
<?php
namespace Meoran\Images\Templates;
use Intervention\Image\Constraint;
use Intervention\Image\Filters\FilterInterface;
use Intervention\Image\Image;
class Large implements FilterInterface
{
public function applyFilter(Image $image)
{
return $image->resize(1920, null, function (Constraint $constraint) {
$constraint->upsize();
$constraint->aspectRatio();
});
}
}

18
src/Templates/Medium.php Executable file
View File

@ -0,0 +1,18 @@
<?php
namespace Meoran\Images\Templates;
use Intervention\Image\Constraint;
use Intervention\Image\Filters\FilterInterface;
use Intervention\Image\Image;
class Medium implements FilterInterface
{
public function applyFilter(Image $image)
{
return $image->resize(240, null, function (Constraint $constraint) {
$constraint->upsize();
$constraint->aspectRatio();
});
}
}

18
src/Templates/Small.php Executable file
View File

@ -0,0 +1,18 @@
<?php
namespace Meoran\Images\Templates;
use Intervention\Image\Constraint;
use Intervention\Image\Filters\FilterInterface;
use Intervention\Image\Image;
class Small implements FilterInterface
{
public function applyFilter(Image $image)
{
return $image->resize(120, null, function (Constraint $constraint) {
$constraint->upsize();
$constraint->aspectRatio();
});
}
}

14
src/functions.php Executable file
View File

@ -0,0 +1,14 @@
<?php
if (!function_exists('config_path')) {
/**
* Get the configuration path.
*
* @param string $path
* @return string
*/
function config_path($path = '')
{
return app()->basePath() . '/config' . ($path ? '/' . $path : $path);
}
}