Shoora
4/24/2019 - 3:58 AM

Laravel Command for Scan Used/Unused Asset Files

Laravel Command for Scan Used/Unused Asset Files

<?php

use Illuminate\Console\Command;
use Symfony\Component\Console\Input\InputOption;
use Symfony\Component\Console\Input\InputArgument;

class AssetsScannerCommand extends Command {

	/**
	 * The console command name.
	 *
	 * @var string
	 */
	protected $name = 'asset:scan';

	/**
	 * The console command description.
	 *
	 * @var string
	 */
	protected $description = 'Find used/unused asset files in application';

	/**
	 * Create a new command instance.
	 *
	 * @return void
	 */
	public function __construct()
	{
		parent::__construct();
	}

	/**
	 * Execute the console command.
	 *
	 * @return mixed
	 */
	public function fire()
	{
		$list_extensions = [
			'html', 'css', 'js', 
			'jpg', 'bmp', 'png', 'bmp', 'gif',
			'mp3', 'mp4', 'mkv'
		];

		$used_or_unused = $this->argument("used_or_unused");
		$scan_dir = app_path($this->option("dir"));
		$extensions = $this->option("extensions");
		
		switch($this->option("remove")) {
			case "all": $action = "actionRemove";
				break;
			case "ask": $action = "actionAskRemove";
				break;
			default: $action = "actionShow";
		}

		if(!$extensions) {
			$extensions = $list_extensions;
		} else {
			$extensions = explode('|', $extensions);
		}

		if(!in_array($used_or_unused, ['used', 'unused'])) {
			throw new \InvalidArgumentException("First argument only can be 'used' or 'unused'");
		}

		// scan php files in application directory
		$php_files = $this->findRecursive($scan_dir, function($file) {
			$ext = pathinfo($file, PATHINFO_EXTENSION);
			if("php" == $ext) {
				return $file;
			}
		});

		$assets = array();

		// scan use of asset() function in each php files		
		foreach($php_files as $php_file) {
			$files = $this->scanUsedAssets($php_file);
			$assets = array_merge($assets, $files);
		}

		// if unused, scan public dir, and then select inverse using array_diff
		if("unused" === $used_or_unused) {
			$all_assets = $this->scanPublicDir();
			$assets = array_diff($all_assets, $assets);
		}
		
		$assets = array_unique($assets, SORT_STRING);

		// run actions
		$n = 0;
		foreach($assets as $i => $asset) {
			$ext = pathinfo($asset, PATHINFO_EXTENSION);
			
			if(in_array($ext, $extensions)) {
				$this->{$action}($n+1, $asset);
				$n++;
			}
		}

		echo "\n";
	}

	protected function actionAskRemove($n, $file)
	{
		$sure = $this->ask(str_pad($n, 5, ' ', STR_PAD_LEFT)." > Want remove '{$file}'? [N|y]");
		
		if("y" === strtolower($sure)) {
			unlink(public_path($file));
		}
	}

	protected function actionRemove($n, $file)
	{
		$this->info(str_pad($n, 5, ' ', STR_PAD_LEFT)." > Removing '{$file}'");
		unlink(public_path($file));
	}

	protected function actionShow($n, $file)
	{
		$this->info(str_pad($n, 5, ' ', STR_PAD_LEFT)." > {$file}");
	}

	protected function scanUsedAssets($file)
	{
		$file_content = file_get_contents($file);
		// http://en.wikipedia.org/wiki/Filename#Reserved_characters_and_words
		preg_match_all("/asset\([\'\"](?<filename>[a-zA-Z0-9.#@&()\[\]{}_ \/-]+)[\'\"]\)/", $file_content, $match);

		$filenames = $match['filename'];

		return $filenames;
	}

	protected function scanPublicDir()
	{
		return $this->findRecursive(public_path(), function($file) {
			return str_replace(public_path().'/', '', $file);
		});
	}

	protected function findRecursive($path, Closure $matcher)
	{
		$files = array_diff(scandir($path), ['.','..']);

		$matchs = array();

		foreach($files as $file) {
			$filepath = $path.'/'.$file;

			$matchname = $matcher($filepath);

			if(!empty($matchname) AND !is_dir($filepath)) {
				$matchs[] = $matchname;
			}

			if(is_dir($filepath)) {
				$matchs = array_merge($matchs, $this->findRecursive($filepath, $matcher));
			}
		}

		return $matchs;
	}

	/**
	 * Get the console command arguments.
	 *
	 * @return array
	 */
	protected function getArguments()
	{
		return array(
			array('used_or_unused', InputArgument::REQUIRED, 'Used or unused assets'),
		);
	}

	/**
	 * Get the console command options.
	 *
	 * @return array
	 */
	protected function getOptions()
	{
		return array(
			array('dir', 'd', InputOption::VALUE_OPTIONAL, 'Scanned directory from app (default is "views")', 'views'),
			array('extensions', 'e', InputOption::VALUE_OPTIONAL, 'Showing only specified file extension, example "js|css|png"', null),
			array('remove', 'r', InputOption::VALUE_OPTIONAL, 'Remove list assets, can be "all" or "ask"', null),
		);
	}

}