[PHP]symlink関数を完全解説!シンボリックリンクを作成する方法

PHP

こんにちは!今回は、PHPの標準関数であるsymlink()について詳しく解説していきます。シンボリックリンク(ソフトリンク)を作成できる、ファイルシステム管理に便利な関数です!

symlink関数とは?

symlink()関数は、シンボリックリンク(symbolic link)を作成する関数です。

シンボリックリンクは、別のファイルやディレクトリを指す特殊なファイルで、ショートカットのような役割を果たします。デプロイメント、バージョン管理、パス管理など、様々な場面で活用されます!

基本的な構文

symlink(string $target, string $link): bool
  • $target: リンク先のパス(リンクが参照するファイルやディレクトリ)
  • $link: 作成するシンボリックリンクのパス
  • 戻り値: 成功時はtrue、失敗時はfalse

シンボリックリンクとは?

// シンボリックリンクの概念
// 
// 元のファイル: /var/www/app/releases/v1.0.0/index.php
// シンボリックリンク: /var/www/app/current → v1.0.0
//
// currentにアクセスすると、実際にはv1.0.0にアクセスされる

// 特徴:
// - リンク先が削除されても、リンク自体は残る(デッドリンク)
// - ディレクトリへのリンクも作成可能
// - 絶対パスでも相対パスでも指定可能
// - Windows ではサポートが制限的

基本的な使用例

シンプルなリンク作成

// ファイルへのシンボリックリンク
$target = '/tmp/original.txt';
$link = '/tmp/link.txt';

// 元のファイルを作成
file_put_contents($target, 'Hello, World!');

// シンボリックリンクを作成
if (symlink($target, $link)) {
    echo "シンボリックリンクを作成しました\n";
} else {
    echo "シンボリックリンクの作成に失敗しました\n";
}

// リンクを通してファイルを読む
echo file_get_contents($link);  // "Hello, World!"

ディレクトリへのリンク

// ディレクトリへのシンボリックリンク
$targetDir = '/var/www/app/releases/v1.0.0';
$linkDir = '/var/www/app/current';

if (symlink($targetDir, $linkDir)) {
    echo "ディレクトリのシンボリックリンクを作成しました\n";
}

// リンクを通してディレクトリにアクセス
$files = scandir($linkDir);
print_r($files);

相対パスでのリンク

// 相対パスを使用
$target = '../data/config.php';  // 相対パス
$link = '/tmp/config-link.php';

if (symlink($target, $link)) {
    echo "相対パスでシンボリックリンクを作成しました\n";
}

// リンク先の実際のパスを取得
$realPath = realpath($link);
echo "実際のパス: {$realPath}\n";

リンクの確認

$link = '/tmp/link.txt';

// シンボリックリンクかどうか確認
if (is_link($link)) {
    echo "{$link} はシンボリックリンクです\n";
    
    // リンク先を取得
    $target = readlink($link);
    echo "リンク先: {$target}\n";
}

// ファイルとして存在するか確認
if (file_exists($link)) {
    echo "リンク先のファイルが存在します\n";
}

リンクの削除

$link = '/tmp/link.txt';

// シンボリックリンクを削除(リンク先は削除されない)
if (unlink($link)) {
    echo "シンボリックリンクを削除しました\n";
}

// 注意: unlinkはリンク自体を削除し、リンク先には影響しない

エラーハンドリング

$target = '/tmp/original.txt';
$link = '/tmp/link.txt';

// リンクが既に存在する場合
if (file_exists($link)) {
    echo "リンクが既に存在します\n";
    unlink($link);  // 既存のリンクを削除
}

// リンクを作成
if (symlink($target, $link)) {
    echo "成功\n";
} else {
    $error = error_get_last();
    echo "失敗: {$error['message']}\n";
}

実践的な使用例

例1: リンク管理システム

class SymlinkManager {
    /**
     * シンボリックリンクを作成
     */
    public static function create($target, $link, $overwrite = false) {
        // リンクが既に存在する場合
        if (file_exists($link) || is_link($link)) {
            if (!$overwrite) {
                return [
                    'success' => false,
                    'error' => 'Link already exists'
                ];
            }
            
            // 既存のリンクを削除
            unlink($link);
        }
        
        // ターゲットの存在確認
        if (!file_exists($target)) {
            return [
                'success' => false,
                'error' => 'Target does not exist'
            ];
        }
        
        // シンボリックリンクを作成
        if (symlink($target, $link)) {
            return [
                'success' => true,
                'target' => $target,
                'link' => $link
            ];
        }
        
        return [
            'success' => false,
            'error' => error_get_last()['message'] ?? 'Unknown error'
        ];
    }
    
    /**
     * リンク先を取得
     */
    public static function getTarget($link) {
        if (!is_link($link)) {
            return null;
        }
        
        return readlink($link);
    }
    
    /**
     * リンクを更新(新しいターゲットに変更)
     */
    public static function update($link, $newTarget) {
        if (!is_link($link)) {
            return [
                'success' => false,
                'error' => 'Not a symlink'
            ];
        }
        
        // 既存のリンクを削除
        unlink($link);
        
        // 新しいリンクを作成
        return self::create($newTarget, $link);
    }
    
    /**
     * リンクを削除
     */
    public static function remove($link) {
        if (!is_link($link)) {
            return [
                'success' => false,
                'error' => 'Not a symlink'
            ];
        }
        
        if (unlink($link)) {
            return [
                'success' => true,
                'removed' => $link
            ];
        }
        
        return [
            'success' => false,
            'error' => 'Failed to remove symlink'
        ];
    }
    
    /**
     * リンク情報を取得
     */
    public static function getInfo($link) {
        if (!is_link($link)) {
            return null;
        }
        
        $target = readlink($link);
        $stat = lstat($link);
        
        return [
            'link' => $link,
            'target' => $target,
            'target_exists' => file_exists($target),
            'real_path' => realpath($link),
            'created' => $stat['ctime'],
            'modified' => $stat['mtime']
        ];
    }
    
    /**
     * リンクが有効か確認(リンク先が存在するか)
     */
    public static function isValid($link) {
        if (!is_link($link)) {
            return false;
        }
        
        $target = readlink($link);
        return file_exists($target);
    }
    
    /**
     * デッドリンクを検出
     */
    public static function isDeadLink($link) {
        return is_link($link) && !file_exists($link);
    }
}

// 使用例
echo "=== シンボリックリンク管理 ===\n";

$target = '/tmp/original.txt';
$link = '/tmp/mylink.txt';

// ファイルを作成
file_put_contents($target, 'Sample content');

// リンクを作成
$result = SymlinkManager::create($target, $link);
if ($result['success']) {
    echo "リンクを作成しました\n";
} else {
    echo "エラー: {$result['error']}\n";
}

// リンク情報を取得
$info = SymlinkManager::getInfo($link);
if ($info) {
    echo "\nリンク情報:\n";
    echo "  リンク: {$info['link']}\n";
    echo "  ターゲット: {$info['target']}\n";
    echo "  有効: " . ($info['target_exists'] ? 'Yes' : 'No') . "\n";
    echo "  実パス: {$info['real_path']}\n";
}

// リンクの有効性チェック
echo "\n有効なリンク: " . (SymlinkManager::isValid($link) ? 'Yes' : 'No') . "\n";

// ターゲットを削除してデッドリンクをテスト
unlink($target);
echo "デッドリンク: " . (SymlinkManager::isDeadLink($link) ? 'Yes' : 'No') . "\n";

// クリーンアップ
SymlinkManager::remove($link);

例2: デプロイメントシステム

class DeploymentManager {
    private $baseDir;
    private $releasesDir;
    private $currentLink;
    
    public function __construct($baseDir) {
        $this->baseDir = $baseDir;
        $this->releasesDir = $baseDir . '/releases';
        $this->currentLink = $baseDir . '/current';
        
        // ディレクトリ構造を作成
        if (!is_dir($this->releasesDir)) {
            mkdir($this->releasesDir, 0755, true);
        }
    }
    
    /**
     * 新しいリリースをデプロイ
     */
    public function deploy($version, $files) {
        $releaseDir = $this->releasesDir . '/' . $version;
        
        // リリースディレクトリを作成
        if (!mkdir($releaseDir, 0755, true)) {
            return [
                'success' => false,
                'error' => 'Failed to create release directory'
            ];
        }
        
        // ファイルをコピー
        foreach ($files as $source => $destination) {
            $targetPath = $releaseDir . '/' . $destination;
            $targetDir = dirname($targetPath);
            
            if (!is_dir($targetDir)) {
                mkdir($targetDir, 0755, true);
            }
            
            copy($source, $targetPath);
        }
        
        // currentリンクを更新
        $this->switchVersion($version);
        
        return [
            'success' => true,
            'version' => $version,
            'path' => $releaseDir
        ];
    }
    
    /**
     * バージョンを切り替え
     */
    public function switchVersion($version) {
        $releaseDir = $this->releasesDir . '/' . $version;
        
        if (!is_dir($releaseDir)) {
            return [
                'success' => false,
                'error' => 'Release does not exist'
            ];
        }
        
        // 既存のcurrentリンクを削除
        if (is_link($this->currentLink)) {
            unlink($this->currentLink);
        }
        
        // 新しいリンクを作成
        if (symlink($releaseDir, $this->currentLink)) {
            return [
                'success' => true,
                'version' => $version,
                'current' => $this->currentLink
            ];
        }
        
        return [
            'success' => false,
            'error' => 'Failed to create symlink'
        ];
    }
    
    /**
     * 現在のバージョンを取得
     */
    public function getCurrentVersion() {
        if (!is_link($this->currentLink)) {
            return null;
        }
        
        $target = readlink($this->currentLink);
        return basename($target);
    }
    
    /**
     * 利用可能なバージョン一覧
     */
    public function listVersions() {
        if (!is_dir($this->releasesDir)) {
            return [];
        }
        
        $versions = [];
        $items = scandir($this->releasesDir);
        
        foreach ($items as $item) {
            if ($item === '.' || $item === '..') {
                continue;
            }
            
            $path = $this->releasesDir . '/' . $item;
            if (is_dir($path)) {
                $versions[] = [
                    'version' => $item,
                    'path' => $path,
                    'is_current' => ($item === $this->getCurrentVersion())
                ];
            }
        }
        
        return $versions;
    }
    
    /**
     * 古いリリースを削除
     */
    public function cleanOldReleases($keep = 3) {
        $versions = $this->listVersions();
        
        // 現在のバージョンを除外
        $versions = array_filter($versions, function($v) {
            return !$v['is_current'];
        });
        
        // 日付でソート(新しい順)
        usort($versions, function($a, $b) {
            return filemtime($b['path']) <=> filemtime($a['path']);
        });
        
        // 保持する数を超えたリリースを削除
        $removed = [];
        for ($i = $keep; $i < count($versions); $i++) {
            $this->removeDirectory($versions[$i]['path']);
            $removed[] = $versions[$i]['version'];
        }
        
        return [
            'removed' => $removed,
            'kept' => array_slice(array_column($versions, 'version'), 0, $keep)
        ];
    }
    
    /**
     * ディレクトリを再帰的に削除
     */
    private function removeDirectory($dir) {
        if (!is_dir($dir)) {
            return;
        }
        
        $items = scandir($dir);
        
        foreach ($items as $item) {
            if ($item === '.' || $item === '..') {
                continue;
            }
            
            $path = $dir . '/' . $item;
            
            if (is_dir($path)) {
                $this->removeDirectory($path);
            } else {
                unlink($path);
            }
        }
        
        rmdir($dir);
    }
    
    /**
     * ロールバック(前のバージョンに戻す)
     */
    public function rollback() {
        $versions = $this->listVersions();
        $current = $this->getCurrentVersion();
        
        // 現在のバージョンを除外
        $versions = array_filter($versions, function($v) use ($current) {
            return $v['version'] !== $current;
        });
        
        // 日付でソート(新しい順)
        usort($versions, function($a, $b) {
            return filemtime($b['path']) <=> filemtime($a['path']);
        });
        
        if (empty($versions)) {
            return [
                'success' => false,
                'error' => 'No previous version available'
            ];
        }
        
        // 最新の前バージョンに切り替え
        $previousVersion = $versions[0]['version'];
        return $this->switchVersion($previousVersion);
    }
}

// 使用例
echo "=== デプロイメントシステム ===\n";

$deployment = new DeploymentManager('/tmp/app');

// バージョン1.0.0をデプロイ
$files = [
    '/tmp/source/index.php' => 'index.php',
    '/tmp/source/config.php' => 'config.php'
];

// ファイルを準備(テスト用)
@mkdir('/tmp/source', 0755, true);
file_put_contents('/tmp/source/index.php', '<?php echo "v1.0.0";');
file_put_contents('/tmp/source/config.php', '<?php return ["version" => "1.0.0"];');

$result = $deployment->deploy('v1.0.0', $files);
if ($result['success']) {
    echo "v1.0.0をデプロイしました\n";
}

// 現在のバージョンを確認
$current = $deployment->getCurrentVersion();
echo "現在のバージョン: {$current}\n";

// バージョン一覧
echo "\n利用可能なバージョン:\n";
foreach ($deployment->listVersions() as $version) {
    $marker = $version['is_current'] ? ' (current)' : '';
    echo "  - {$version['version']}{$marker}\n";
}

// v1.1.0をデプロイ
file_put_contents('/tmp/source/index.php', '<?php echo "v1.1.0";');
$deployment->deploy('v1.1.0', $files);
echo "\nv1.1.0をデプロイしました\n";
echo "現在のバージョン: " . $deployment->getCurrentVersion() . "\n";

// ロールバック
$rollback = $deployment->rollback();
if ($rollback['success']) {
    echo "\nロールバックしました\n";
    echo "現在のバージョン: " . $deployment->getCurrentVersion() . "\n";
}

例3: バージョン管理システム

class VersionManager {
    private $versionsDir;
    private $activeLink;
    
    public function __construct($baseDir) {
        $this->versionsDir = $baseDir . '/versions';
        $this->activeLink = $baseDir . '/active';
        
        if (!is_dir($this->versionsDir)) {
            mkdir($this->versionsDir, 0755, true);
        }
    }
    
    /**
     * 新しいバージョンを作成
     */
    public function createVersion($version, $content) {
        $versionPath = $this->versionsDir . '/' . $version;
        
        if (file_exists($versionPath)) {
            return [
                'success' => false,
                'error' => 'Version already exists'
            ];
        }
        
        if (file_put_contents($versionPath, $content) !== false) {
            return [
                'success' => true,
                'version' => $version,
                'path' => $versionPath
            ];
        }
        
        return [
            'success' => false,
            'error' => 'Failed to create version'
        ];
    }
    
    /**
     * バージョンをアクティブに設定
     */
    public function setActive($version) {
        $versionPath = $this->versionsDir . '/' . $version;
        
        if (!file_exists($versionPath)) {
            return [
                'success' => false,
                'error' => 'Version does not exist'
            ];
        }
        
        // 既存のリンクを削除
        if (is_link($this->activeLink)) {
            unlink($this->activeLink);
        }
        
        // 新しいリンクを作成
        if (symlink($versionPath, $this->activeLink)) {
            return [
                'success' => true,
                'version' => $version,
                'active' => $this->activeLink
            ];
        }
        
        return [
            'success' => false,
            'error' => 'Failed to set active version'
        ];
    }
    
    /**
     * アクティブなバージョンを取得
     */
    public function getActiveVersion() {
        if (!is_link($this->activeLink)) {
            return null;
        }
        
        $target = readlink($this->activeLink);
        return basename($target);
    }
    
    /**
     * アクティブなバージョンの内容を読む
     */
    public function readActive() {
        if (!is_link($this->activeLink)) {
            return null;
        }
        
        return file_get_contents($this->activeLink);
    }
    
    /**
     * すべてのバージョンを一覧表示
     */
    public function listVersions() {
        if (!is_dir($this->versionsDir)) {
            return [];
        }
        
        $versions = [];
        $items = scandir($this->versionsDir);
        $active = $this->getActiveVersion();
        
        foreach ($items as $item) {
            if ($item === '.' || $item === '..') {
                continue;
            }
            
            $path = $this->versionsDir . '/' . $item;
            
            if (is_file($path)) {
                $versions[] = [
                    'version' => $item,
                    'is_active' => ($item === $active),
                    'size' => filesize($path),
                    'modified' => filemtime($path)
                ];
            }
        }
        
        // バージョン番号でソート
        usort($versions, function($a, $b) {
            return version_compare($a['version'], $b['version']);
        });
        
        return $versions;
    }
    
    /**
     * バージョンを比較
     */
    public function compareVersions($version1, $version2) {
        $path1 = $this->versionsDir . '/' . $version1;
        $path2 = $this->versionsDir . '/' . $version2;
        
        if (!file_exists($path1) || !file_exists($path2)) {
            return null;
        }
        
        $content1 = file_get_contents($path1);
        $content2 = file_get_contents($path2);
        
        return [
            'version1' => $version1,
            'version2' => $version2,
            'identical' => $content1 === $content2,
            'size_diff' => strlen($content1) - strlen($content2)
        ];
    }
}

// 使用例
echo "=== バージョン管理システム ===\n";

$vm = new VersionManager('/tmp/config');

// バージョンを作成
$vm->createVersion('1.0.0', 'setting1=value1');
$vm->createVersion('1.0.1', 'setting1=value1\nsetting2=value2');
$vm->createVersion('1.1.0', 'setting1=value1\nsetting2=value2\nsetting3=value3');

// バージョン一覧
echo "利用可能なバージョン:\n";
foreach ($vm->listVersions() as $version) {
    $marker = $version['is_active'] ? ' (active)' : '';
    echo "  {$version['version']}{$marker} - " . 
         date('Y-m-d H:i:s', $version['modified']) . "\n";
}

// v1.0.1をアクティブに設定
$vm->setActive('1.0.1');
echo "\nアクティブバージョン: " . $vm->getActiveVersion() . "\n";
echo "内容:\n" . $vm->readActive() . "\n";

// バージョン比較
$comparison = $vm->compareVersions('1.0.0', '1.0.1');
if ($comparison) {
    echo "\n{$comparison['version1']} vs {$comparison['version2']}:\n";
    echo "  同一: " . ($comparison['identical'] ? 'Yes' : 'No') . "\n";
    echo "  サイズ差: {$comparison['size_diff']} bytes\n";
}

例4: パス管理ツール

class PathManager {
    /**
     * 共通ディレクトリへのショートカットを作成
     */
    public static function createShortcut($name, $target, $shortcutsDir = '/tmp/shortcuts') {
        if (!is_dir($shortcutsDir)) {
            mkdir($shortcutsDir, 0755, true);
        }
        
        $link = $shortcutsDir . '/' . $name;
        
        // 既存のショートカットを削除
        if (is_link($link)) {
            unlink($link);
        }
        
        if (symlink($target, $link)) {
            return [
                'success' => true,
                'shortcut' => $link,
                'target' => $target
            ];
        }
        
        return [
            'success' => false,
            'error' => 'Failed to create shortcut'
        ];
    }
    
    /**
     * ショートカット一覧を取得
     */
    public static function listShortcuts($shortcutsDir = '/tmp/shortcuts') {
        if (!is_dir($shortcutsDir)) {
            return [];
        }
        
        $shortcuts = [];
        $items = scandir($shortcutsDir);
        
        foreach ($items as $item) {
            if ($item === '.' || $item === '..') {
                continue;
            }
            
            $link = $shortcutsDir . '/' . $item;
            
            if (is_link($link)) {
                $target = readlink($link);
                $shortcuts[] = [
                    'name' => $item,
                    'link' => $link,
                    'target' => $target,
                    'valid' => file_exists($target)
                ];
            }
        }
        
        return $shortcuts;
    }
    
    /**
     * ショートカットを削除
     */
    public static function removeShortcut($name, $shortcutsDir = '/tmp/shortcuts') {
        $link = $shortcutsDir . '/' . $name;
        
        if (!is_link($link)) {
            return [
                'success' => false,
                'error' => 'Shortcut does not exist'
            ];
        }
        
        if (unlink($link)) {
            return [
                'success' => true,
                'removed' => $link
            ];
        }
        
        return [
            'success' => false,
            'error' => 'Failed to remove shortcut'
        ];
    }
    
    /**
     * デッドリンクをクリーンアップ
     */
    public static function cleanDeadLinks($shortcutsDir = '/tmp/shortcuts') {
        $shortcuts = self::listShortcuts($shortcutsDir);
        $removed = [];
        
        foreach ($shortcuts as $shortcut) {
            if (!$shortcut['valid']) {
                unlink($shortcut['link']);
                $removed[] = $shortcut['name'];
            }
        }
        
        return [
            'removed' => $removed,
            'count' => count($removed)
        ];
    }
}

// 使用例
echo "=== パス管理ツール ===\n";

// ディレクトリを作成
@mkdir('/tmp/projects/myapp', 0755, true);
@mkdir('/tmp/projects/webapp', 0755, true);
@mkdir('/tmp/logs', 0755, true);

// ショートカットを作成
PathManager::createShortcut('myapp', '/tmp/projects/myapp');
PathManager::createShortcut('webapp', '/tmp/projects/webapp');
PathManager::createShortcut('logs', '/tmp/logs');

// ショートカット一覧
echo "ショートカット一覧:\n";
foreach (PathManager::listShortcuts() as $shortcut) {
    $status = $shortcut['valid'] ? '✓' : '✗';
    echo "  {$status} {$shortcut['name']} → {$shortcut['target']}\n";
}

// ディレクトリを削除してデッドリンクを作成
rmdir('/tmp/logs');

// デッドリンクをクリーンアップ
$cleanup = PathManager::cleanDeadLinks();
echo "\nクリーンアップ: {$cleanup['count']}個のデッドリンクを削除\n";

例5: プラグインシステム

class PluginManager {
    private $pluginsDir;
    private $enabledDir;
    
    public function __construct($baseDir) {
        $this->pluginsDir = $baseDir . '/available';
        $this->enabledDir = $baseDir . '/enabled';
        
        if (!is_dir($this->pluginsDir)) {
            mkdir($this->pluginsDir, 0755, true);
        }
        
        if (!is_dir($this->enabledDir)) {
            mkdir($this->enabledDir, 0755, true);
        }
    }
    
    /**
     * プラグインをインストール
     */
    public function install($pluginName, $pluginPath) {
        $targetPath = $this->pluginsDir . '/' . $pluginName;
        
        if (file_exists($targetPath)) {
            return [
                'success' => false,
                'error' => 'Plugin already installed'
            ];
        }
        
        if (copy($pluginPath, $targetPath)) {
            return [
                'success' => true,
                'plugin' => $pluginName,
                'path' => $targetPath
            ];
        }
        
        return [
            'success' => false,
            'error' => 'Failed to install plugin'
        ];
    }
    
    /**
     * プラグインを有効化
     */
    public function enable($pluginName) {
        $sourcePath = $this->pluginsDir . '/' . $pluginName;
        $linkPath = $this->enabledDir . '/' . $pluginName;
        
        if (!file_exists($sourcePath)) {
            return [
                'success' => false,
                'error' => 'Plugin not installed'
            ];
        }
        
        if (is_link($linkPath)) {
            return [
                'success' => false,
                'error' => 'Plugin already enabled'
            ];
        }
        
        if (symlink($sourcePath, $linkPath)) {
            return [
                'success' => true,
                'plugin' => $pluginName,
                'enabled' => true
            ];
        }
        
        return [
            'success' => false,
            'error' => 'Failed to enable plugin'
        ];
    }
    
    /**
     * プラグインを無効化
     */
    public function disable($pluginName) {
        $linkPath = $this->enabledDir . '/' . $pluginName;
        
        if (!is_link($linkPath)) {
            return [
                'success' => false,
                'error' => 'Plugin not enabled'
            ];
        }
        
        if (unlink($linkPath)) {
            return [
                'success' => true,
                'plugin' => $pluginName,
                'enabled' => false
            ];
        }
        
        return [
            'success' => false,
            'error' => 'Failed to disable plugin'
        ];
    }
    
    /**
     * インストール済みプラグインの一覧
     */
    public function listAvailable() {
        $plugins = [];
        $items = scandir($this->pluginsDir);
        
        foreach ($items as $item) {
            if ($item === '.' || $item === '..') {
                continue;
            }
            
            $path = $this->pluginsDir . '/' . $item;
            if (is_file($path)) {
                $plugins[] = [
                    'name' => $item,
                    'enabled' => $this->isEnabled($item)
                ];
            }
        }
        
        return $plugins;
    }
    
    /**
     * 有効化されているプラグインの一覧
     */
    public function listEnabled() {
        $plugins = [];
        $items = scandir($this->enabledDir);
        
        foreach ($items as $item) {
            if ($item === '.' || $item === '..') {
                continue;
            }
            
            $link = $this->enabledDir . '/' . $item;
            if (is_link($link)) {
                $plugins[] = $item;
            }
        }
        
        return $plugins;
    }
    
    /**
     * プラグインが有効化されているか確認
     */
    public function isEnabled($pluginName) {
        $linkPath = $this->enabledDir . '/' . $pluginName;
        return is_link($linkPath);
    }
    
    /**
     * プラグインをアンインストール
     */
    public function uninstall($pluginName) {
        // 有効化されている場合は無効化
        if ($this->isEnabled($pluginName)) {
            $this->disable($pluginName);
        }
        
        $pluginPath = $this->pluginsDir . '/' . $pluginName;
        
        if (!file_exists($pluginPath)) {
            return [
                'success' => false,
                'error' => 'Plugin not installed'
            ];
        }
        
        if (unlink($pluginPath)) {
            return [
                'success' => true,
                'plugin' => $pluginName
            ];
        }
        
        return [
            'success' => false,
            'error' => 'Failed to uninstall plugin'
        ];
    }
}

// 使用例
echo "=== プラグインシステム ===\n";

$pm = new PluginManager('/tmp/plugins');

// プラグインファイルを作成(テスト用)
@mkdir('/tmp/plugin-source', 0755, true);
file_put_contents('/tmp/plugin-source/analytics.php', '<?php // Analytics Plugin');
file_put_contents('/tmp/plugin-source/cache.php', '<?php // Cache Plugin');
file_put_contents('/tmp/plugin-source/seo.php', '<?php // SEO Plugin');

// プラグインをインストール
$pm->install('analytics.php', '/tmp/plugin-source/analytics.php');
$pm->install('cache.php', '/tmp/plugin-source/cache.php');
$pm->install('seo.php', '/tmp/plugin-source/seo.php');

echo "インストール済みプラグイン:\n";
foreach ($pm->listAvailable() as $plugin) {
    $status = $plugin['enabled'] ? '[有効]' : '[無効]';
    echo "  {$status} {$plugin['name']}\n";
}

// プラグインを有効化
$pm->enable('analytics.php');
$pm->enable('cache.php');

echo "\n有効なプラグイン:\n";
foreach ($pm->listEnabled() as $plugin) {
    echo "  - {$plugin}\n";
}

// プラグインを無効化
$pm->disable('cache.php');

echo "\nプラグイン状態:\n";
foreach ($pm->listAvailable() as $plugin) {
    $status = $plugin['enabled'] ? '[有効]' : '[無効]';
    echo "  {$status} {$plugin['name']}\n";
}

例6: キャッシュディレクトリ管理

class CacheDirectoryManager {
    private $cacheBaseDir;
    private $currentLink;
    
    public function __construct($cacheBaseDir) {
        $this->cacheBaseDir = $cacheBaseDir;
        $this->currentLink = $cacheBaseDir . '/current';
        
        if (!is_dir($cacheBaseDir)) {
            mkdir($cacheBaseDir, 0755, true);
        }
    }
    
    /**
     * 新しいキャッシュディレクトリを作成
     */
    public function createCacheDir() {
        $timestamp = time();
        $cacheDirName = 'cache_' . $timestamp;
        $cacheDirPath = $this->cacheBaseDir . '/' . $cacheDirName;
        
        if (!mkdir($cacheDirPath, 0755, true)) {
            return [
                'success' => false,
                'error' => 'Failed to create cache directory'
            ];
        }
        
        return [
            'success' => true,
            'path' => $cacheDirPath,
            'name' => $cacheDirName
        ];
    }
    
    /**
     * キャッシュディレクトリをアクティブに設定
     */
    public function setActive($cacheDirName) {
        $cacheDirPath = $this->cacheBaseDir . '/' . $cacheDirName;
        
        if (!is_dir($cacheDirPath)) {
            return [
                'success' => false,
                'error' => 'Cache directory does not exist'
            ];
        }
        
        // 既存のリンクを削除
        if (is_link($this->currentLink)) {
            unlink($this->currentLink);
        }
        
        // 新しいリンクを作成
        if (symlink($cacheDirPath, $this->currentLink)) {
            return [
                'success' => true,
                'active' => $cacheDirName
            ];
        }
        
        return [
            'success' => false,
            'error' => 'Failed to set active cache directory'
        ];
    }
    
    /**
     * キャッシュをアトミックに更新
     */
    public function atomicUpdate($dataCallback) {
        // 新しいキャッシュディレクトリを作成
        $result = $this->createCacheDir();
        if (!$result['success']) {
            return $result;
        }
        
        $newCacheDir = $result['path'];
        
        // データを生成
        try {
            $dataCallback($newCacheDir);
        } catch (Exception $e) {
            // エラーが発生したら新しいディレクトリを削除
            $this->removeDirectory($newCacheDir);
            return [
                'success' => false,
                'error' => $e->getMessage()
            ];
        }
        
        // アトミックに切り替え
        $this->setActive($result['name']);
        
        return [
            'success' => true,
            'cache_dir' => $newCacheDir
        ];
    }
    
    /**
     * 古いキャッシュディレクトリを削除
     */
    public function cleanOldCache($keep = 2) {
        $items = scandir($this->cacheBaseDir);
        $cacheDirs = [];
        
        foreach ($items as $item) {
            if ($item === '.' || $item === '..' || $item === 'current') {
                continue;
            }
            
            $path = $this->cacheBaseDir . '/' . $item;
            if (is_dir($path) && strpos($item, 'cache_') === 0) {
                $cacheDirs[] = [
                    'name' => $item,
                    'path' => $path,
                    'time' => filemtime($path)
                ];
            }
        }
        
        // 時間でソート(新しい順)
        usort($cacheDirs, function($a, $b) {
            return $b['time'] <=> $a['time'];
        });
        
        // 保持する数を超えたディレクトリを削除
        $removed = [];
        for ($i = $keep; $i < count($cacheDirs); $i++) {
            $this->removeDirectory($cacheDirs[$i]['path']);
            $removed[] = $cacheDirs[$i]['name'];
        }
        
        return [
            'removed' => $removed,
            'count' => count($removed)
        ];
    }
    
    /**
     * ディレクトリを再帰的に削除
     */
    private function removeDirectory($dir) {
        if (!is_dir($dir)) {
            return;
        }
        
        $items = scandir($dir);
        
        foreach ($items as $item) {
            if ($item === '.' || $item === '..') {
                continue;
            }
            
            $path = $dir . '/' . $item;
            
            if (is_dir($path)) {
                $this->removeDirectory($path);
            } else {
                unlink($path);
            }
        }
        
        rmdir($dir);
    }
}

// 使用例
echo "=== キャッシュディレクトリ管理 ===\n";

$cache = new CacheDirectoryManager('/tmp/app-cache');

// アトミックにキャッシュを更新
$result = $cache->atomicUpdate(function($cacheDir) {
    // キャッシュデータを生成
    file_put_contents($cacheDir . '/data1.json', json_encode(['key' => 'value1']));
    file_put_contents($cacheDir . '/data2.json', json_encode(['key' => 'value2']));
    echo "キャッシュデータを生成しました\n";
});

if ($result['success']) {
    echo "キャッシュを更新しました: {$result['cache_dir']}\n";
}

// もう一度更新
sleep(1);
$cache->atomicUpdate(function($cacheDir) {
    file_put_contents($cacheDir . '/data1.json', json_encode(['key' => 'updated_value1']));
    file_put_contents($cacheDir . '/data2.json', json_encode(['key' => 'updated_value2']));
});

echo "\n2回目の更新完了\n";

// 古いキャッシュをクリーンアップ
$cleanup = $cache->cleanOldCache(1);
echo "\nクリーンアップ: {$cleanup['count']}個の古いキャッシュを削除\n";

link()関数との違い

// symlink() - シンボリックリンク(ソフトリンク)
// link() - ハードリンク

$original = '/tmp/original.txt';
file_put_contents($original, 'Hello');

// シンボリックリンク
$symlink = '/tmp/symlink.txt';
symlink($original, $symlink);

// ハードリンク
$hardlink = '/tmp/hardlink.txt';
link($original, $hardlink);

// 違い:
// 1. シンボリックリンクは別のファイル(リンク先へのポインタ)
// 2. ハードリンクは同じファイルへの別の名前
// 3. 元ファイルを削除した場合:
//    - シンボリックリンクはデッドリンクになる
//    - ハードリンクは有効なまま

unlink($original);

// シンボリックリンクはデッドリンク
echo is_link($symlink) ? "リンク存在\n" : "リンク消失\n";  // リンク存在
echo file_exists($symlink) ? "ファイル存在\n" : "ファイル消失\n";  // ファイル消失

// ハードリンクは有効
echo file_exists($hardlink) ? "ファイル存在\n" : "ファイル消失\n";  // ファイル存在

まとめ

symlink()関数の特徴をまとめると:

できること:

  • シンボリックリンクの作成
  • ファイル/ディレクトリへのリンク
  • 相対パス/絶対パスの指定

利点:

  • デプロイメントの簡素化
  • バージョン管理
  • パスの抽象化
  • アトミックな切り替え

推奨される使用場面:

  • デプロイメントシステム
  • バージョン管理
  • プラグイン管理
  • キャッシュ管理
  • ショートカット作成

注意点:

  • リンク先が削除されるとデッドリンクになる
  • Windowsでは制限あり
  • パーミッションに注意
  • 既存リンクは上書きされない

関連関数:

  • readlink(): リンク先を取得
  • is_link(): リンクかチェック
  • link(): ハードリンク作成
  • unlink(): リンク削除
  • realpath(): 実際のパスを取得
  • lstat(): リンク自体の情報取得

よく使うパターン:

// デプロイメント
symlink('/releases/v1.0.0', '/current');

// バージョン切り替え
unlink('/current');
symlink('/releases/v1.0.1', '/current');

// リンクの確認
if (is_link($path)) {
    $target = readlink($path);
}

// デッドリンクのチェック
if (is_link($path) && !file_exists($path)) {
    echo "デッドリンク\n";
}

symlink()は、デプロイメントやバージョン管理において非常に重要な関数です。アトミックな切り替えや柔軟なパス管理を実現し、システムの信頼性を高めます!

タイトルとURLをコピーしました