Мой родной PHP шаблонизатор

Целью этой статьи является разработка простого шаблонизатора, который смог бы удовлетворить основные потребности разработчика в моём лице, да и не только в моём. Принцип работы его будет основан на native методе, который вместе с другим детальнее описан тут для тех, кто еще не в курсе.

Итак, какой функционал я хочу? Для начала, это должно быть довольно простое решение без всяких рюшиков типа "куча хелперов". Конечно, кому-то это нужно, но у меня обычно всё проще, поэтому обойдёмся. То есть, должно быть независимое решение в виде одного класса. Сразу скажу, что кеширование результатов тоже упустим, дабы не отвлекаться от сути, да и задача эта в большинстве случаев принадлежит другим. Кому надо, легко может его прикрутить как внутри так и снаружи, используя этот материал. Далее я также хочу спокойно выполнять такие вещи как:

Удобное извлечение переменных в шаблоне
Вставка шаблона в шаблон
Корректная работа с HTML кодом
Режим отладки
Этим списком, думаю, я буду полностью удовлетворен.

Начнем с переменных. Тут, как и везде далее все просто - магический метод __get нам поможет обращаться к переменной по ее имени. Всё начинает выглядеть примерно так:


Listing №1 (PHP)

class Template {
  /**
   * @var array
   */
  private $_Vars;
  /**
   * @var string
   */
  private $_Name;
  /**
   * @var string
   */
  private $_Path;
 
  public function __construct()
  {
    $this->_Name = '';
    $this->_Vars = array();
    $this->_Path = '';
  }
  /**
   * @param string $VarName имя переменной
   * @param string $VarValue значение переменной
   * @return Template
   */
  public function AddVar($VarName, $VarValue)
  {
    if ($VarName !== '') {
      $this->_Vars[$VarName] = $VarValue;
    }
    return $this;
  }
  /**
   * @param string $Varname имя переменной
   * @return mixed
   */
  public function __get($VarName)
  {
    if (array_key_exists($VarName, $this->_Vars)) {
      if ($this->_Vars[$VarName] instanceof Template) {
        return $this->_Vars[$VarName]->Prepare();
      }
      else{
        return $this->_Vars[$VarName];
      }
    }
    return NULL;
  }
  /**
   * @var string $Name имя шаблона
   */
  public function SetName($Name)
  {
    $this->_Name = $Name;
  }
  /**
   * @var string $Path путь к шаблону(ам)
   */
  public function SetPath($Path)
  {
    $this->_Path = $Path;
  }
}


Теперь двумя следующими методами обеспечим корректность выводимых данных. Первый заботиться об спецсимволах HTML, а второй о валидности ccылок:


Listing №2 (PHP)
  /**
   * @param mixed $Var
   * @return mixed
   */
  protected function EscapeHtml($Var)
  {
    if(is_array($Var)) {
      foreach($Var as &$VarItem) {
        $VarItem = $this->EscapeHtml($VarItem);
      }
    }
    else {
      $Var = htmlspecialchars($Var, ENT_QUOTES);
    }
    return $Var;
  }
  /**
   * @param mixed $Var
   * @return mixed
   */
  protected function EscapeUrl($Var)
  {
    if(is_array($Var)) {
      foreach($Var as &$VarItem) {
        $VarItem = $this->EscapeUrl($Var);
      }
    }
    else {
      $Var = htmlentities($Var, ENT_QUOTES);
    }
    return $Var;
  }


Идем дальше.

Что я подразумеваю под словами "режим отладки"? Это, скорее всего, больше похоже на режим удобного чтения. В таком режиме на выходе мы получим код шаблона в том же виде, что и до его парсинга, то есть со всеми табуляциями, переводами строк и прочей нечистью. Это довольно удобно, потому что native шаблоны могут сложно восприниматься и то и дело приходится всё табулировать и переносить. В противоположность этому режиму будет обычный рабочий режим, где ненужные символы будут удалены за ненадобностью. За это будет отвечать метод _Zip, а за переключение между режимами статический метод SetDebug:


Listing №3 (PHP)
  /**
   * Свойство хранящее текущий режим
   * @static
   * @var bool
   */
  private static $_Debug = FALSE;
  /**
   * Вкл/выкл дебаг режим
   * @static
   * @param bool $TrueOrFalse TRUE
   */
  public static function SetDebug($TrueOrFalse = TRUE)
  {
    self::$_Debug = $TrueOrFalse;
  }
  /**
   * Возвращает $Text с удаленными "\t", "\n" и "\r"
   * @param  string $Text
   * @return string
   */
  private function _Zip($Text)
  {
    return (empty($Text)) ? $Text : str_replace(array("\t", "\n", "\r"), '', $Text);
  }


Теперь нам нужен метод Prepare, который выполнит всю основную работу, но не выведет данные, а возвратит их строкой. Это нам понадобиться для реализации вставки шаблона в шаблон, которая и происходит при извлечении переменной в шаблоне (метод __get). Думаю, без буферизации вывода тут не обойтись:


Listing №4 (PHP)
  /**
   * @return string
   */
  public function Prepare()
  {
    if (file_exists($this->_Path . $this->_Name)) {
      ob_start();
      ${__CLASS__} = $this;
      include $this->_Path . $this->_Name;
      unset(${__CLASS__});
      return (self::$_Debug) ? ob_get_clean() : $this->_Zip(ob_get_clean());
    }
    else {
      throw new Exception('Template file "' . $this->_Path . $this->_Name . '" does not exists');
    }
  }


Сначала проверяем наличие шаблона. Затем "по родному" парсим шаблон и возвращаем контент из буфера, очистив ненужные символы, если это необходимо. Также, меня очень напрягает писать в шаблонах $this, поэтому я решил писать там $Template. Мне так очень нравится.

Наконец, добавив метод непосредственного вывода, получаем нужный "полный фарш":


Listing №5 (PHP)
class Template {
  /**
   * @var string
   */
  private $_Name;
  /**
   * @var array
   */
  private $_Vars;
  /**
   * @var string
   */
  private $_Path;
  /**
   * @static
   * @var bool
   */
  private static $_Debug = FALSE;
  /**
   * @static
   * @param bool $TrueOrFalse TRUE
   */
  public static function SetDebug($TrueOrFalse = TRUE)
  {
    self::$_Debug = $TrueOrFalse;
  }
 
  public function __construct()
  {
    $this->_Name = '';
    $this->_Vars = array();
    $this->_Path = '';
  }
  /**
   * @param string $VarName
   * @param string $VarValue
   * @return Template
   */
  public function AddVar($VarName, $VarValue)
  {
    if ($VarName !== '') {
      $this->_Vars[$VarName] = $VarValue;
    }
    return $this;
  }
  /**
   * @param string $Varname
   * @return mixed
   */
  public function __get($VarName)
  {
    if (array_key_exists($VarName, $this->_Vars)) {
      if ($this->_Vars[$VarName] instanceof Template) {
        return $this->_Vars[$VarName]->Prepare();
      }
      else{
        return $this->_Vars[$VarName];
      }
    }
    return NULL;
  }
  /**
   * @var string $Name
   */
  public function SetName($Name)
  {
    $this->_Name = $Name;
  }
  /**
   * @var string $Path
   */
  public function SetPath($Path)
  {
    $this->_Path = $Path;
  }
  /**
   * @return string
   */
  public function Prepare()
  {
    if (file_exists($this->_Path . $this->_Name)) {
      ob_start();
      ${__CLASS__} = $this;
      include $this->_Path . $this->_Name;
      unset(${__CLASS__});
      return (self::$_Debug) ? ob_get_clean() : $this->_Zip(ob_get_clean());
    }
    else {
      throw new Exception('Template file "' . $this->_Path . $this->_Name . '" does not exists');
    }
  }
 
  public function Display()
  {
    print ($this->Prepare());
  }
  /**
   * @param mixed $Var
   * @return mixed
   */
  protected function EscapeHtml($Var)
  {
    if(is_array($Var)) {
      foreach($Var as &$VarItem) {
        $VarItem = $this->EscapeHtml($VarItem);
      }
    }
    else {
      $Var = htmlspecialchars($Var, ENT_QUOTES);
    }
    return $Var;
  }
  /**
   * @param mixed $Var
   * @return mixed
   */
  protected function EscapeUrl($Var)
  {
    if(is_array($Var)) {
      foreach($Var as &$VarItem) {
        $VarItem = $this->EscapeUrl($Var);
      }
    }
    else {
      $Var = htmlentities($Var, ENT_QUOTES);
    }
    return $Var;
  }
  /**
   * @param  string $Text
   * @return string
   */
  private function _Zip($Text)
  {
    return (empty($Text)) ? $Text : str_replace(array("\t", "\n", "\r"), '', $Text);
  }
}


Ну и напоследок, небольшой пример использования.

Допустим, у нас есть два шаблона - главная страница page.php и список пользователей userslist.php. Нужно отобразить этот список вместе с информацией на главной странице. Поскупившись на контент, рисуем главную:


Listing №6 (PHP)
<div style="border: 1px solid #E0E3F3;">
  <?php echo $Template->UserList;?>
</div>

<a href="<?php echo $Template->EscapeUrl($Template->Link)?>">About</a>

А также список пользователей:


Listing №7 (PHP)
<div>
  <?php foreach($Template->EscapeHtml($Template->UserNames) as $UserName) { ?>
  <span style="color: #FF0000;"><?php echo $UserName;?></span><br>
  <?php } ?>
</div>


За логику отвечает некая модель:


Listing №8 (PHP)
//Подключаем шаблонизатор
require 'Template.php';
//Устанавливаем режим отладки включенным для всех шаблонов
Template::SetDebug(TRUE);
//Инициализируем подшаблон списка пользователей
$UsersListView = new Template();
$UsersListView->SetName('userslist.html');
//Список пользователей
$UserNames = array('Den Rights©', 'Dasha Nasha', 'Samuil Kakojto');
$UsersListView->AddVar('UserNames', $UserNames);
//Инициализируем главный шаблон страницы
$PageView = new Template();
$PageView->SetName('page.html');
$PageView->AddVar('UserList', $UsersListView);
$PageView->AddVar('Link', 'http://www.itdumka.com.ua/index.php?cmd=shownode&node=1');
$PageView->Display();



В результате работы с включенным режимом отладки, в отличие от обычного режима, где все будет в одну строку, мы увидим такую картину:


Listing №9 (HTML)
<div style="border: 1px solid #E0E3F3;">
  <div>
    <span style="color: #FF0000;">Den Rights©</span><br>
    <span style="color: #FF0000;">Dasha Nasha</span><br>
    <span style="color: #FF0000;">Samuil Kakojto</span><br>
  </div>
</div>
<a href="http://www.itdumka.com.ua/index.php?cmd=shownode&node=1">About</a>


Вот так.

Добавлено: 08 Августа 2018 07:45:41 Добавил: Андрей Ковальчук

Кеширование в PHP - теперь немного лучше, чем просто кеш

В этой короткой статье я расскажу, как я убирал тривиальность из политики кеширования на файлах в PHP сценариях. В принципе это всё можно применить и не к "файловому" кешированию. Надеюсь, многим эта статейка принесет пользу.


Итак, что же мне не нравилось в моей тривиальности, и что я хотел бы улучшить?

Во-первых, меня очень напрягала обстановка, когда несколько килобайт кеша вдруг одновременно устаревало и могло довольно прилично нагрузить сервер. А во-вторых, мой мозг до некоторого момента все время мучила мысль о непосредственной работе с файлами, а точнее - создание и удаление.

Все эти беды происходили от выбранного мною тривиального подхода к управлению кешированием. Суть его заключалась в следующем.

Сначала я проверял актуальность кешированных данных и если они таковыми не являлись, то проводил операцию получения данных и записывал "свежий" кеш на диск. Иначе - если кеш был актуален - просто брал данные от туда. Вот пример из моего старенького шаблонизатора:


Listing №1 (PHP)

<?php
if ($this->_Cache->IsActual()) {
  return $this->_Cache->Read();
}
else {
  $Content = $this->Parse($File);
  $this->_Cache->Write($Content);
  return $Content;
}
?>


При этом проверка актуальности выглядела приблизительно так:


Listing №2 (PHP)
<?php
public function IsActual()
{
  clearstatcache();
  if (!file_exists($this->_Id)) {
    return FALSE;
  }
  if (!($CreateTime = filemtime($this->_Id))) {
    return FALSE;
  }
  if (($CreateTime + $this->_Expired) < time()) {
    @unlink($this->_Id);
    return FALSE;
  }
  return TRUE;
}
?>


И вот, в один прекрасный день я решил попытаться избавиться от таких проблем:

Одновременное обновление большого количества кешированных данных. То есть при практически одновременном обращении пользователей в момент устаревания кеша, происходит не только большая нагрузка на сервер но и конфликты кеширования. Большая нагрузка происходит от того, что кеш не может быстро обновиться, так как данных довольно много. Поэтому каждый пользователь фактически заново запускает процедуру обновления кеша, что влечет за собой конфликты, которые в свою очередь оказывают еще большую нагрузку на сервер
Медленное обновление кеша, вследствие его немалого объема. То есть необходимо, большой кеш обновить как можно быстрее
Большое спасибо Джорджу Шлосснейглу, за прекрасный простой приём, используя который можно организовать решение этих двух проблем. По сути Джордж предлагает заменить операции удаления старого кеша и создания нового кеша одной операцией - заменой старого кеша. Стоило немного подумать над приемом Джорджа и из него вытекло решение всех вышеописанных проблем:

Разбиваем большой кешь на отдельные куски
Заранее готовим каждый из кусков в разные моменты времени, маскируя наличие нового кеша
Заменяем старый кеш на новый быстрым методом
Вот и всё - очень просто и со вкусом.

Теперь, по традиции, непосредственно сама реализация. Она далека от идеала и предназначена всего лишь для объяснения сути. Но все же давайте раздуем примерчик интерфейсом для backend-а. А также, в связи с тем, что я все данные получаю из функций и методов, я "приноровлю" механизм кеширования к методам получения возвращаемых результатов.

Для начала сконструируем интерфейс с нужными функциями:


Listing №3 (PHP)
<?php
interface Cachebackend {
  /**
   * @param string $Key
   */
  public function GetCache($Key);
  /**
   * @param string $Key
   * @param int    $ExpiredPeriod
   */
  public function IsActual($Key, $ExpiredPeriod);
  /**
   * @param string $Key
   * @param int    $ExpiredPeriod
   * @param int    $SoonPeriod
   * @param string $SoonFuseKeyPostfix
   */
  public function IsNeedCreateNewCache($Key, $ExpiredPeriod, $SoonPeriod, $SoonFuseKeyPostfix);
  /**
   * @param string $Key
   * @param string $Data
   */
  public function PutCache($Key, $Data);
  /**
   * @param stirng $PreparedKey
   * @param string $RenameKey
   * @return bool
   */
  public function Rename($PreparedKey, $RenameKey);
}
?>


Теперь сконструируем непосредственный каркас с нашим алгоритмом:


Listing №4 (PHP)
<?php
class Cacher {
  /**
   * @var Cachebackend
   */
  private static $_Backend = NULL;
  /**
   * @var mixed
   */
  private static $_CallbackSignature = array();
  /**
   * @var array
   */
  private static $_CallbackArguments = array();
  /**
   * @static
   * @param string $Tag
   * @param string $Key
   * @param int    $ExpiredPeriod
   * @param int    $SoonPeriod
   * @param sting  $SoonFuseKeyPostfix '.next'
   * @return mixed
   */
  public static function GetData($Tag, $Key, $ExpiredPeriod, $SoonPeriod, $SoonFuseKeyPostfix = '.next')
  {
    $Key = CACHE_PATH . strtolower($Tag) . '_' . strtolower($Key) . CACHE_EXT;
    if (NULL === self::$_Backend) {
      self::$_Backend = new Cachefilebackend;
    }
    if (self::$_Backend->IsActual($Key, $ExpiredPeriod)) {
      $Data = self::$_Backend->GetCache($Key);
      if(self::$_Backend->IsNeedCreateNewCache($Key, $ExpiredPeriod, $SoonPeriod, $SoonFuseKeyPostfix)) {
        $CallbackData = self::_GetCallbackResult();
        self::$_Backend->PutCache($Key . $SoonFuseKeyPostfix, $CallbackData);
      }
      return $Data;
    }
    else {
      if(self::$_Backend->IsActual($Key . $SoonFuseKeyPostfix, $ExpiredPeriod)) {
        $Data = self::$_Backend->GetCache($Key . $SoonFuseKeyPostfix);
        self::$_Backend->Rename($Key . $SoonFuseKeyPostfix, $Key);
        return $Data;
      }
      else {
        $CallbackData = self::_GetCallbackResult();
        self::$_Backend->PutCache($Key, $CallbackData);
        return $CallbackData;
      }
    }
  }
  /**
   * @static
   * @param string $CallbackFunction
   * @param array $CallbackArguments
   * @param mixed $CallbackObject NULL
   */
  public static function SetCallback($CallbackFunction, $CallbackArguments, $CallbackObject = NULL)
  {
    if ($CallbackObject) {
      self::$_CallbackSignature = array($CallbackObject, $CallbackFunction);
    }
    else {
      self::$_CallbackSignature = $CallbackFunction;
    }
    self::$_CallbackArguments = $CallbackArguments;
  }
  /**
   * @static
   * @return mixed
   */
  private static function _GetCallbackResult()
  {
    self::_CheckCallback();
    return call_user_func_array(self::$_CallbackSignature, self::$_CallbackArguments);
  }
  /**
   * @static
   */
  private static function _CheckCallback()
  {
    if(!is_callable(self::$_CallbackSignature, FALSE, $CallableName)) {
      throw new Exception($CallableName . ' is not correct callback');
    }
    if(!is_array(self::$_CallbackArguments)) {
      throw new Exception('Callback arguments must be an array');
    }
  }
}
?>


Давайте немного разберемся, что к чему.

Итак, основной метод - это метод GetData. Что мы передаем ему:

$Tag - идентификатор группы кеш-данных. Предназначен для управления целой группой кеш-данных.
$Key - идентификатор конкретных кеш-данных. Однозначно идентифицирует кешируемые данные.
$ExpiredPeriod - время жизни кеша. Это время можно даже назвать как время "псевдо" жизни, ведь новый кеш появится до истечения этого времени, хотя старый и удалиться именно по этому значению.
$SoonPeriod - период времени, который определяет временной интервал между появлением нового кеша и удалением старого: T(удаления старого кеша) - T(создания нового кеша) = $SoonPeriod
$SoonFuseKeyPostfix - параметр по умолчанию, обозначающий расширение файлов нового кеша, чтобы не путать их с еще существующими файлами старого кеша.
Константы CACHE_PATH и CACHE_EXT определяют кеш-директорию и расширение кеш-файлов соответственно. Метод GetData создает объект backend-а и выполняет соответствующие действия по управлению кешем:

Проверяет актуальности кеша и если он таков, то отдает его данные
Создает новый кешь, если время для необходимости его создания наступило
Если кешированные данные не актуальны и новый кешь создан, то происходит замена старого кеша на новый и отдача соответствующих данных из нового кеша
Если ни один из кешей не актуален то выполняется естественное получение данных, а также запись их в кеш
Таким образом с помощью параметра $SoonPeriod мы можем каждый кеш-кусок обновить в разные моменты времени, и практически безболезненно выдать новую информацию в один момент. Конечно же, это не избавит нас полностью от вышеописанных проблем, но все же при некоторых экспериментах со значениями $SoonPeriod для конкретного проекта поможет снизить нагрузку на сервер.

Функция SetCallback может принимать как метод объекта, так и функцию для получения кешируемых данных. Остальные две функции выполняют внутреннюю работу.

Теперь опишем файловый backend, который следует вышеобъявленному интерфейсу:


Listing №5 (PHP)
<?php
class Cachefilebackend implements Cachebackend {
  /**
   * @see Cachebackend
   */
  public function GetCache($Key)
  {
    clearstatcache();
    if (file_exists($Key)) {
      $Content = '';
      $Content = @file_get_contents($Key);
      return unserialize((string)$Content);
    }
    else {
      throw new Exception('Cached keyfile "' . $Key. '" does not exists');
    }
  }
  /**
   * @see Cachebackend
   */
  public function IsActual($Key, $ExpiredPeriod)
  {
    clearstatcache();
    if (file_exists($Key)) {
      if (time() - filemtime($Key) > $ExpiredPeriod) {
        @unlink($Key);
        return FALSE;
      }
      else {
        return TRUE;
      }
    }
    return FALSE;
  }
  /**
   * @see Cachebackend
   */
  public function IsNeedCreateNewCache($Key, $ExpiredPeriod, $SoonPeriod, $SoonFuseKeyPostfix)
  {
    clearstatcache();
    if (file_exists($Key)) {
      if ((time() - filemtime($Key) > $ExpiredPeriod - $SoonPeriod) && (
          (!file_exists($Key . $SoonFuseKeyPostfix)))) {
        return TRUE;
      }
      else {
        return FALSE;
      }
    }
    else {
      return FALSE;
    }
  }
  /**
   * @see Cachebackend
   */
  public function PutCache($Key, $Data)
  {
    if($Key) {
      @file_put_contents($Key, serialize($Data));
    }
    else {
      throw new Exception('Cache key is empty');
    }
  }
  /**
   * @see Cachebackend
   */
  public function Rename($PreparedKey, $RenameKey)
  {
    return @rename($PreparedKey, $RenameKey);
  }
}
?>


Думаю теперь надо привести пример использования. Допустим, у вас есть объект $UserData, который возвращает одни данные c помощью метода GetData(), и функция GetUserText(), возвращающая другие данные. В обе функции передается параметр - идентификатор пользователя. Результат их работы складывается как строки и отдается на съедение браузеру. Мы закешируем эти данные на день и подготовим кеш объекта за час до конца дня, а кеш функции за два часа.


Listing №6 (PHP)
<?php
//Инициализируем пользователя
$User = new User();
//Инициализируем данные пользователя
$UserData= new UserData($User->Id);
//Настраиваем кеширование данных пользователя
Cacher::SetCallback('GetData', array($User->Id), $UserData);
//Получаем данные пользователя
$Response = Cacher::GetData('userdata', md5($User->Id), 24 * 60 * 60, 60 * 60);
//Настраиваем кеширование текста пользователя
Cacher::SetCallback('GetUserText', array($User->Id));
//Получаем текст пользователя
$Response .= Cacher::GetData('usertext', md5($User->Id), 24 * 60 * 60, 120 * 60);
//Отдаем результат
echo $Response;
?>


Естественно, все сработает как мы хотели только если хотя бы один пользователь обратиться к серверу не ранее чем за час до начала нового дня, поэтому выбор времени создания нового кеша зависит от множества факторов конкретного проекта. Так что всё в ваших руках. Спасибо за внимание.

Добавлено: 08 Августа 2018 07:42:08 Добавил: Андрей Ковальчук

Программа: Поиск сайта

Программу site-search.php, показанную в примере 19.5, можно использовать как поисковую машину для сайтов от малого до среднего размеров, основанных на файлах. Программа ищет условия поиска (в $_REQUEST['term']) во всех файлах в пределах определенного набора каталогов в корневом каталоге документов. Эти каталоги определяются в $search_dirs. Программа также последовательно заходит в подкаталоги и следует символическим ссылкам, но запоминает, какие файлы и каталоги она просмотрела, так что она не зацикливается.

Если найдены какие-либо страницы, содержащие условия поиска, то она печатает список ссылок на эти страницы, расположенные в соответствии с алфавитным порядком заголовков этих страниц. Если у страницы нет заголовка (между тегами <title> и </title>), то используется URI страницы относительно корневого документального каталога. Программа ищет условия поиска между тегами и в каждом файле. Если на вашей странице между тегами находится большой объем текста, который вы хотите исключить из поиска, то окружите текст, который следует искать, специальными HTML-коммен-тариями, а затем модифицируйте $body_regex так, чтобы искать эти теги. Например, ваша страница выглядит следующим образом:


// Некоторые HTML-элементы для меню, заголовков и т. д.
<!-- search-start -->
<h1>Aliens Invade Earth</h1>
<h3>by H.G. Wells</h3>
<p>Aliens invaded earth today. Uh Oh.</p>
// Продолжение повествования
<!-- search-end -->
// Некоторые HTML-элементы для нижних колонтитулов и т. д.



Чтобы сравнивать условия поиска только с названием, автором и с изложением внутри HTML-комментариев, измените $body_regex на:

$body_regex = '#<!-- search-start -->(.*' .
preg_quote($_REQUEST['term'],'#').
'.*)<!-- search-end -->#Sis';


Если вы не хотите, чтобы условия поиска сравнивались с текстом внутри тегов HTML или PHP на вашей странице, то добавьте вызов функции strip_tags() в программу, загружающую содержимое файла для поиска:

// загружаем содержимое файла в переменную $file
$file = strip_tags(join('',file($path)));

Пример 19.5. site-search.php
function pc_search_dir($dir) {
global $body_regex,$title_regex,$seen;
// массив для хранения соответствующих страниц
$pages = array();
// массив для хранения каталогов поиска
$dirs = array();
// отмечаем этот каталог как просмотренный,
// чтобы не просматривать его снова
$seen[realpath($dir)] = true;
// если мы можем получить дескриптор этого каталога
if (is_readable($dir) && ($d = dir($dir))) {
// берем каждое имя файла в каталоге
while (false !== ($f = $d->read())) {
// строим полный путь к файлу
$path = $d->path.'/'.$f;
// если это обычный файл и мы можем прочитать его
if (is_file($path) && is_readable($path)) {
$realpath = realpath($path);
// если мы уже видели этот файл,
if ($seen[$realpath]) {
// то пропускаем его
continue;
} else {
// в противном случае отмечаем его как просмотренный,
// чтобы пропустить его, если придем к нему снова
$seen[$realpath] = true;
}
// загружаем содержимое файла в переменную $file
$file = join('',file($path));
// если условия поиска находятся внутри
// ограничителей body
if (preg_match($body_regex,$file)) {
// конструируем относительный URI файла, удаляя
// документальный корневой каталог из полного пути
$uri = substr_replace($path,'',0,strlen
($_SERVER['DOCUMENT_ROOT']));
// Если страница имеет название, то находим его
if (preg_match('#<title>(.*?)</title>#Sis',$file,$match)) {
// и добавляем название и URI в переменную $pages
array_push($pages,array($uri,$match));
} else {
// в противном случае используем URI
// в качестве названия
array_push($pages,array($uri,$uri));
}
}
} else {
// если элемент каталога – это допустимый подкаталог
if (is_dir($path) && ('.' != $f) && ('..' != $f)) {
// добавляем его к списку каталогов для просмотра
array_push($dirs,$path);
}
}
}
$d->close();
}
/* Просматриваем каждый файл в каждом подкаталоге данного каталога
* и добавляем найденные страницы из этих каталогов
* к переменной $pages. Просматриваем только подкаталог,
* который мы еще не видели.
*/
foreach ($dirs as $subdir) {
$realdir = realpath($subdir);
if (! $seen[$realdir]) {
$seen[$realdir] = true;
$pages = array_merge($pages,pc_search_dir($subdir));
}
}
return $pages;
}
// вспомогательная функция для сортировки найденных страниц
// в алфавитном порядке их названий
function pc_page_sort($a,$b) {
if ($a == $b) {
return strcmp($a[0],$b[0]);
} else {
return ($a > $b);
}
}
// массив для хранения страниц, соответствующих условиям поиска
$matching_pages = array();
// массив для хранения страниц во время сканирования условий поиска
$seen = array();
// каталоги для поиска ниже корневого документального каталога
$search_dirs = array('sports','movies','food');
// Регулярное выражение для использования в искомых файлах. Модификатор
// шаблона «S» говорит машине PCRE «изучать» regex для большей эффективности.
$body_regex = '#(.*' . preg_quote($_REQUEST['term'],'#').
'.*)#Sis';
// добавляем соответствующие файлы, найденные в каждом каталоге,
// в $matching_pages
foreach ($search_dirs as $dir) {
$matching_pages = array_merge($matching_pages,
pc_search_dir($_SERVER['DOCUMENT_ROOT'].'/'.$dir));
}
if (count($matching_pages)) {
// сортируем найденные страницы по названию
usort($matching_pages,'pc_page_sort');
print '<ul>';
// выводим каждое название со ссылкой на страницу
foreach ($matching_pages as $k => $v) {
print sprintf('<li> %s',$v[0],$v);
}
print '</ul>';
} else {
print 'No pages found.';
}

Добавлено: 22 Июля 2018 18:47:24 Добавил: Андрей Ковальчук

Программа: Перечень каталогов веб-сервера

Программа web-ls.php, показанная в примере 19.4, выводит сведения о файлах корневого каталога документов веб-сервера, отформатированные аналогично выводу UNIX-команды ls. Ссылки на имена файлов выполнены так, чтобы можно было загрузить каждый файл, а
ссылки на имена каталогов позволяют просматривать каждый каталог.

Большинство строк предназначены для построения легкого для чтения представления прав доступа к файлу, но суть программы в цикле while в ее конце. Метод $d->read() дает имя каждого файла в каталоге.

Затем функция lstat() извлекает информацию, а функция printf() выводит отформатированную информацию об этом файле. Функция mode_string() и константы, которые она использует, преобразуют восьмеричное представление режима файла (т. е. 35316) в более легкую для чтения строку (т. е. -rwsrw-r--).

Пример 19.4. web-ls.php

/* Битовые маски для определения прав доступа к файлу и типа.
* Имена и значения, перечисленные ниже, POSIX-совместимы, отдельные системы
* могут иметь свои собственные расширения.
*/
define('S_IFMT',0170000); // маска для всех типов
define('S_IFSOCK',0140000); // тип: сокет
define('S_IFLNK',0120000); // тип: символическая ссылка
define('S_IFREG',0100000); // тип: обычный файл
define('S_IFBLK',0060000); // тип: блочное устройство
define('S_IFDIR',0040000); // тип: каталог
define('S_IFCHR',0020000); // тип: символьное устройство
define('S_IFIFO',0010000); // тип: fifo
define('S_ISUID',0004000); // бит смены идентификатора
пользователя (set-uid)
define('S_ISGID',0002000); // бит смены идентификатора группы (set-gid)
define('S_ISVTX',0001000); // sticky-бит (липкий бит)
define('S_IRWXU',00700); // маска для прав доступа владельца
define('S_IRUSR',00400); // владелец: право на чтение
define('S_IWUSR',00200); // владелец: право на запись
define('S_IXUSR',00100); // владелец: право на выполнение
define('S_IRWXG',00070); // маска для прав доступа группы
define('S_IRGRP',00040); // группа: право на чтение
define('S_IWGRP',00020); // группа: право на запись
define('S_IXGRP',00010); // группа: право на выполнение
define('S_IRWXO',00007); // маска для прав доступа остальных
define('S_IROTH',00004); // остальные: право на чтение
define('S_IWOTH',00002); // остальные: право на запись
define('S_IXOTH',00001); // остальные: право на выполнение

/* mode_string() – это вспомогательная функция, которая принимает
* восьмеричный режим и возвращает десятисимвольную строку, представляющую
* тип файла и права доступа, которые соответствуют восьмеричному режиму.
* Это PHP-версия функции mode_string() в пакете файловых утилит GNU.
*/
function mode_string($mode) {
$s = array();
// set type letter
if (($mode & S_IFMT) == S_IFBLK) {
$s[0] = 'b';
} elseif (($mode & S_IFMT) == S_IFCHR) {
$s[0] = 'c';
} elseif (($mode & S_IFMT) == S_IFDIR) {
$s[0] = 'd';
} elseif (($mode & S_IFMT) == S_IFREG) {
$s[0] = '-';
} elseif (($mode & S_IFMT) == S_IFIFO) {
$s[0] = 'p';
} elseif (($mode & S_IFMT) == S_IFLNK) {
$s[0] = 'l';
} elseif (($mode & S_IFMT) == S_IFSOCK) {
$s[0] = 's';
}
// устанавливаем права доступа пользователя
$s = $mode & S_IRUSR ? 'r' : '-';
$s = $mode & S_IWUSR ? 'w' : '-';
$s = $mode & S_IXUSR ? 'x' : '-';
// устанавливаем права доступа группы
$s = $mode & S_IRGRP ? 'r' : '-';
$s = $mode & S_IWGRP ? 'w' : '-';
$s = $mode & S_IXGRP ? 'x' : '-';
// устанавливаем права доступа остальных
$s = $mode & S_IROTH ? 'r' : '-';
$s = $mode & S_IWOTH ? 'w' : '-';
$s = $mode & S_IXOTH ? 'x' : '-';
// устанавливаем символы выполнения для set-uid, set-gid и sticky
if ($mode & S_ISUID) {
if ($s != 'x') {
// set-uid, но не исполняемый владельцем
$s = 'S';
} else {
$s = 's';
}
}
if ($mode & S_ISGID) {
if ($s != 'x') {
// set-gid, но не исполняемый группой
$s = 'S';
} else {
$s = 's';
}
}
if ($mode & S_ISVTX) {
if ($s != 'x') {
// sticky, но не исполняемый остальными
$s = 'T';
} else {
$s = 't';
}
}
// возвращаем отформатированную строку
return join('',$s);
}
// Начинаем в корневом документальном каталоге, если ничего не указано
if (isset($_REQUEST['dir'])) {
$dir = $_REQUEST['dir'];
} else {
$dir = '';
}
// находим $dir в файловой системе
$real_dir = realpath($_SERVER['DOCUMENT_ROOT'].$dir);
// проверяем, что $real_dir находится в документальном корневом каталоге
if (! preg_match('/^'.preg_quote($_SERVER['DOCUMENT_ROOT'],'/').'/',
$real_dir)) {
die("$dir is not inside the document root");
}
// канонизируем $dir, удаляя из его начала корневой документальный каталог
$dir = substr_replace($real_dir,'',0,strlen($_SERVER['DOCUMENT_ROOT']));
// мы открываем каталог?
if (! is_dir($real_dir)) {
die("$real_dir is not a directory");
}
// открываем указанный каталог
$d = dir($real_dir) or die("can't open $real_dir: $php_errormsg");
print '<table>';
// читаем каждое вхождение в каталоге
while (false !== ($f = $d->read())) {
// получаем информацию об этом файле
$s = lstat($d->path.'/'.$f);
// переводим идентификатор пользователя в имя пользователя
$user_info = posix_getpwuid($s['uid']);
// переводим идентификатор группы в имя группы
$group_info = posix_getgrgid($s['gid']);
// форматируем данные для улучшения читаемости
$date = strftime('%b %e %H:%M',$s['mtime']);
// переводим восьмеричный режим в читаемую строку
$mode = mode_string($s['mode']);
$mode_type = substr($mode,0,1);
if (($mode_type == 'c') || ($mode_type == 'b')) {
/* если это блочное или символьное устройство, то выводим старший
* и младший тип устройства вместо размера файла */
$major = ($s['rdev'] >> 8) & 0xff;
$minor = $s['rdev'] & 0xff;
$size = sprintf('%3u, %3u',$major,$minor);
} else {
$size = $s['size'];
}
// форматируем вокруг имени файла
// нет ссылки на текущий каталог
if ('.' == $f) {
$href = $f;
} else {
// не включаем «..» в ссылку на родительский каталог
if ('..' == $f) {
$href = urlencode(dirname($dir));
} else {
$href = urlencode($dir) . '/' . urlencode($f);
}
/* все, за исключением "/", должно быть в кодировке URL */
$href = str_replace('%2F','/',$href);
// просматриваем другие каталоги с помощью веб-команды ls
if (is_dir(realpath($d->path . '/' . $f))) {
$href = sprintf('%s',
$_SERVER['PHP_SELF'],$href,$f);
} else {
// ссылка на файлы для их загрузки
$href= sprintf('%s',$href,$f);
}
// если это ссылка, то показываем также цель
if ('l' == $mode_type) {
$href .= ' -> ' . readlink($d->path.'/'.$f);
}
}
// выводим соответствующую информацию об этом файле
printf('<tr><td>%s</td><td>%3u</td><td align="right">%s</td>
<td align="right">%s</td><td align="right">%s</td>
<td align="right">%s</td><td>%s</td></tr>',
$mode, // форматированная строка режима
$s['nlink'], // количество ссылок на этот файл
$user_info['name'], // имя пользователя-владельца
$group_info['name'], // имя группы
$size, // размер файла (или номера устройства)
$date, // дата и время последнего изменения
$href); // ссылка для просмотра или загрузки
}
print '</table>';

Добавлено: 22 Июля 2018 18:45:15 Добавил: Андрей Ковальчук

Удаление каталога и его содержимого

Задача
Необходимо удалить каталог и все его содержимое, включая подкаталоги и их содержимое.

Решение
В UNIX надо выполнить команду rm:

$directory = escapeshellarg($directory);
exec("rm -rf $directory");
В Windows – команду rmdir:
$directory = escapeshellarg($directory);
exec("rmdir /s /q $directory");


Обсуждение
Очевидно, что удаление файлов может быть опасным. Не забудьте вызвать функцию escapeshellarg() для переменной $directory, чтобы не удалить непредусмотренные файлы.
Поскольку встроенная в PHP функция rmdir(), которая удаляет каталоги, работает только с пустыми каталогами, а функция unlink() не принимает групповые символы оболочки, то вызов системной программы намного легче, чем рекурсивное выполнение цикла по всем файлам в каталоге с их удалением и последующим удалением каждого каталога. Однако если внешняя утилита недоступна, то для удаления каждого подкаталога можно модифицировать функцию.

Добавлено: 22 Июля 2018 18:44:39 Добавил: Андрей Ковальчук

Создание новых каталогов

Задача
Необходимо создать каталог.

Решение
Это делается при помощи функции mkdir():mkdir('/tmp/apples',0777) or die($php_errormsg);

Обсуждение
Второй аргумент функции mkdir() – это режим доступа к новому каталогу, который должен быть восьмеричным числом. Текущая маска (umask) вычитается из этого значения прав доступа для создания прав доступа к новому каталогу. Поэтому если текущая маска равна 0002, вызов mkdir('/tmp/apples',0777) устанавливает права доступа к результирующему каталогу, равные 0775 (пользователь и группа могут читать, писать и выполнять; остальные могут только читать и выполнять).

Встроенная в PHP функция mkdir() может создать новый каталог, только если существует родительский каталог. Например, если /tmp/a не существует, то нельзя создать каталог /tmp/a/b, пока не будет создан каталог /tmp/a. Для создания каталога и его родителя существует два способа: можно вызвать системную программу mkdir или вызвать
функцию pc_mkdir_parents(), показанную в примере 19.3. Чтобы использовать системную программу mkdir в UNIX, выполните:

system('/bin/mkdir -p '.escapeshellarg($directory));

В Windows:
system('mkdir '.escapeshellarg($directory));


Можно также вызвать функцию pc_mkdir_parents(), показанную в примере 19.3.
Пример 19.3. pc_mkdir_parents()
function pc_mkdir_parents($d,$umask = 0777) {
$dirs = array($d);
$d = dirname($d);
$last_dirname = '';
while($last_dirname != $d) {
array_unshift($dirs,$d);
$last_dirname = $d;
$d = dirname($d);
}
foreach ($dirs as $dir) {
if (! file_exists($dir)) {
if (! mkdir($dir,$umask)) {
error_log("Can't make directory: $dir");
return false;
}
} elseif (! is_dir($dir)) {
error_log("$dir is not a directory");
return false;
}
}
return true;
}


Например:
pc_mkdir_parents('/usr/local/upload/test',0777);

Добавлено: 22 Июля 2018 18:44:00 Добавил: Андрей Ковальчук

Обработка всех файлов в каталоге

Задача
Необходимо что-нибудь сделать со всеми файлами в каталоге и в любых его подкаталогах.

Решение
Функция pc_process_dir(), показанная в примере 19.1, возвращает список всех файлов в данном каталоге и ниже.

Пример 19.1. pc_process_dir()

function pc_process_dir($dir_name,$max_depth = 10,$depth = 0) {
if ($depth >= $max_depth) {
error_log("Reached max depth $max_depth in $dir_name.");
return false;
}
$subdirectories = array();
$files = array();
if (is_dir($dir_name) && is_readable($dir_name)) {
$d = dir($dir_name);
while (false !== ($f = $d->read())) {
// пропускаем . и ..
if (('.' == $f) || ('..' == $f)) {
continue;
}
if (is_dir("$dir_name/$f")) {
array_push($subdirectories,"$dir_name/$f");
} else {
array_push($files,"$dir_name/$f");
}
}
$d->close();
foreach ($subdirectories as $subdirectory) {
$files = array_merge($files,pc_process_dir($subdirectory,
$max_depth,$depth+1));
}
}
return $files;
}


Обсуждение
Вот пример: если каталог /tmp содержит файлы a и b, а также каталог c, а каталог /tmp/c содержит файлы d и e, то функция pc_process_dir('/tmp') возвращает массив с элементами /tmp/ a, /tmp/b, /tmp/c/d и /tmp/c/e.

Чтобы выполнить операцию над каждым файлом, организуйте цикл по массиву:

$files = pc_process_dir('/tmp');
foreach ($files as $file) {
print "$file was last accessed at ".strftime('%c',fileatime($file))."\n";
}


Можно не возвращать массив файлов, а написать функцию, обрабатывающую их по мере нахождения. Функция pc_process_dir2(), показанная в примере 19.2, делает это, принимая дополнительный аргумент – имя функции, вызываемой для каждого найденного файла.

Пример 19.2. pc_process_dir2()
function pc_process_dir2($dir_name,$func_name,$max_depth = 10,$depth = 0) {
if ($depth >= $max_depth) {
error_log("Reached max depth $max_depth in $dir_name.");
return false;
}
$subdirectories = array();
$files = array();
if (is_dir($dir_name) && is_readable($dir_name)) {
$d = dir($dir_name);
while (false !== ($f = $d->read())) {
// пропускаем . и ..
if (('.' == $f) || ('..' == $f)) {
continue;
}
if (is_dir("$dir_name/$f")) {
array_push($subdirectories,"$dir_name/$f");
} else {
$func_name("$dir_name/$f");
}
}
$d->close();
foreach ($subdirectories as $subdirectory) {
pc_process_dir2($subdirectory,$func_name,$max_depth,$depth+1);
}
}
}


Функция pc_process_dir2() не возвращает список каталогов; вместо этого функция $func_name вызывается с файлом в качестве аргумента. Ниже показано, как вывести время последнего доступа:

function printatime($file) {
print "$file was last accessed at ".strftime('%c',fileatime($file))."\n";
}
pc_process_dir2('/tmp','printatime');


Эти две функции получают один и тот же результат, но вторая требует меньше памяти, поскольку не передаются потенциально большие массивы.

В функциях pc_process_dir() и pc_process_dir2() реализован алгоритм поиска типа «сначала вширь». При выполнении поиска такого типа функции обрабатывают все файлы в текущем каталоге, а затем последовательно переходят в каждый подкаталог. При выполнении поиска типа «сначала вглубь» они заходят в подкаталог сразу же, как только его обнаруживают, независимо от того, остались еще файлы в текущем каталоге или нет. Поиск типа «сначала вширь» более эффективен с точки зрения расходования памяти – перед тем как функция перейдет в подкаталоги, каждый указатель на текущий каталог закрывается (с помощью $d->close()), поэтому одновременно открыт только один указатель на каталог.

Функция is_dir() возвращает true, когда передана символическая ссылка на каталог, поэтому обе версии функций следуют символическим ссылкам во время путешествия вниз по дереву каталогов. Если вы не хотите следовать ссылкам, замените строку:

if (is_dir("$dir_name/$f")) {

на:
if (is_dir("$dir_name/$f") && (! is_link("$dir_name/$f"))) {

Добавлено: 22 Июля 2018 18:43:07 Добавил: Андрей Ковальчук

Получение списка имен файлов, соответствующих шаблону

Задача
Необходимо найти все имена файлов, соответствующие шаблону.

Решение
Если шаблон представляет собой регулярное выражение, то прочитайте каждый файл из каталога и проверьте его имя с помощью функции preg_match():

$d = dir('/tmp') or die($php_errormsg);
while (false !== ($f = $d->read())) {
// соответствует только именам из букв
if (preg_match('/^[a-zA-Z]+$/',$f)) {
print "$f\n";
}
}
$d->close();


Обсуждение
Если шаблон представляет собой универсальный символ оболочки (*.*), используйте оператор обратного апострофа в командах ls (UNIX) или dir (Windows) для получения соответствующих имен файлов.

Для UNIX:

$files = explode("\n",`ls -1 *.gif`);foreach ($files as $file) {
print "$b\n";
}


Для Windows:

$files = explode("\n",`dir /b *.gif`);
foreach ($files as $file) {
print "$b\n";
}

Добавлено: 22 Июля 2018 18:42:06 Добавил: Андрей Ковальчук

Обработка всех файлов в каталоге

Задача
Необходимо выполнить цикл по всем файлам в каталоге. Например, требуется создать элемент select в форме, содержащей список всех файлов в каталоге.

Решение
Получите дескриптор каталога с помощью функции opendir(), а затем извлеките имя каждого файла, вызвав функцию readdir():

$d = opendir('/tmp') or die($php_errormsg);
while (false !== ($f = readdir($d))) {
print "$f\n";
}
closedir($d);


Обсуждение
Фрагмент кода в разделе «Решение» проверяет возвращенное функцией readdir() значение с помощью оператора тождественного неравенства (!==), поэтому этот код работает с именами файлов, которые могут программно интерпретироваться как false, например, если имя файла 0.

Функция readdir() возвращает любой элемент каталога – является ли он файлом, каталогом или чем-нибудь еще (например, ссылкой или сокетом). Сюда же входят метаэлементы «.» (текущий каталог) и «..» (родительский каталог). Для того чтобы вернуть только файлы, вызовите также функцию is_file():

print '<select name="files">';
$d = opendir('/usr/local/upload') or die($php_errormsg);
while (false !== ($f = readdir($d))) {
if (is_file("/usr/local/upload/$f")) {
print '<option> ' . $f . '</option>';
}
}
closedir($d);
print '</select>';


Функция readdir() возвращает только имя файла каждого вхождения каталога, а не полное имя пути, поэтому необходимо предварять именем каталога переменную $f, прежде чем передавать ее функции is_file().

В PHP есть также объектно-ориентированный интерфейс к информации каталога. Функция dir() возвращает объект, в котором можно вызвать методы read(), rewind() и close(), действующие подобно функци-ям readdir(), rewinddir() и closedir(). У него есть также свойство $path, содержащее полное имя пути к открытому каталогу.

Ниже показано, как выполнить цикл по файлам с помощью объектноориентированного интерфейса:

print '<select name="files">';
$d = dir('/usr/local/upload') or die($php_errormsg);
while (false !== ($f = $d->read())) {
if (is_file($d->path.'/'.$f)) {
print '<option> ' . $f . '</option>';
}
}
$d->close();


В этом примере $d->path равно /usr/local/upload.

Добавлено: 22 Июля 2018 18:41:07 Добавил: Андрей Ковальчук

Копирование и перемещение файла

Задача
Необходимо скопировать или переместить файл.

Решение
Для копирования файла применяется функция copy():

copy($old,$new) or die("couldn't copy $old to $new: $php_errormsg");


А функция rename() позволяет перемещать файл:

rename($old,$new) or die("couldn't move $old to $new: $php_errormsg");


Обсуждение
В UNIX функция rename() не может перемещать файл по файловой системе. Чтобы это сделать, скопируйте файл в новое место и удалите старый файл:

if (copy("/tmp/code.c","/usr/local/src/code.c")) {
unlink("/tmp/code.c");
}


Для выполнения множественного копирования или перемещения вызывайте функцию copy() или rename() в цикле. Каждая функция во время текущего вызова может работать только с одним файлом.

Добавлено: 22 Июля 2018 18:40:20 Добавил: Андрей Ковальчук

Удаление файла

Задача
Необходимо удалить файл.

Решение
Это делается при помощи функции unlink():

unlink($file) or die ("can't delete $file: $php_errormsg");


Обсуждение
Функция unlink() может удалять только те файлы, которые может удалить пользователь процесса PHP. Если функция unlink() работает не так, как положено, то следует проверить права доступа к файлу и то, как запускается PHP.

Добавлено: 22 Июля 2018 18:39:37 Добавил: Андрей Ковальчук

Разделение имени файла на составляющие

Задача
Необходимо определить путь к файлу и его имя; например, требуется создать файл в том же каталоге, в котором находится существующий.

Решение
Для выделения имени файла применяется функция basename(), а функция dirname() – для выделения пути к нему:

$full_name = '/usr/local/php/php.ini';
$base = basename($full_name); // значение переменной $base равно php.ini
$dir = dirname($full_name); // значение переменной $dir
равно /usr/local/php


Функция pathinfo() позволяет получить из ассоциативного массива имя каталога, базовое имя и расширение:

$info = pathinfo('/usr/local/php/php.ini');


Обсуждение
Для создания временного файла в каталоге существующего файла вызовите функцию dirname(), чтобы найти каталог, и передайте его функции tempnam():

$dir = dirname($existing_file);
$temp = tempnam($dir,'temp');
$temp_fh = fopen($temp,'w');

Элементы ассоциативного массива, возвращенные функцией pathinfo(), – это dirname, basename и extension:

$info = pathinfo('/usr/local/php/php.ini');
print_r($info);
Array
(
[dirname] => /usr/local/php
[basename] => php.ini
[extension] => ini
)


Можно также передать функции basename() необязательный суффикс, чтобы удалить его из имени файла. В этом случае переменная $base получит значение php:

$base = basename('/usr/local/php/php.ini','.ini');


Применение функций basename(), dirname() и pathinfo() более переносимо, чем просто разделение полного имени файла символом /, поскольку в них используется разделитель, соответствующий операционной системе. В Windows эти функции считают разделителем каталогов и /, и \.

На других платформах используется только символ /.

В PHP нет встроенной функции для обратного объединения в полное имя файла частей, возвращенных функциями basename(), dirname() и pathinfo(). Чтобы сделать это, надо объединить части с помощью символов . и /:

$dirname = '/usr/local/php';
$basename = 'php';
$extension = 'ini';
$full_name = $dirname . '/' . $basename . '.' . $extension;


Можно спокойно передать полученное таким образом полное имя файла файловой функции PHP в Windows, поскольку PHP в Windows принимает символ / в качестве разделителя каталогов.

Добавлено: 22 Июля 2018 18:38:45 Добавил: Андрей Ковальчук

Изменение прав доступа к файлу или его владельца

Задача
Необходимо изменить права доступа к файлу или его владельца. Например, надо не дать возможности другим пользователям увидеть закрытые данные, хранящиеся в файле.

Решение
Для изменения прав доступа к файлу применяется функция chmod():

chmod('/home/user/secrets.txt',0400);


Функция chown() позволяет изменить владельца файла, а функция chgrp() – изменить группу файла:

chown('/tmp/myfile.txt','sklar'); // указываем пользователя по имени
chgrp('/home/sklar/schedule.txt','soccer'); // указываем группу по имени
chown('/tmp/myfile.txt',5001); // указываем пользователя по его идентификатору
chgrp('/home/sklar/schedule.txt',102); // указываем группу по ее идентификатору


Обсуждение
Права доступа, передаваемые функции chmod(), должны быть указаны в виде восьмеричного числа.

Суперпользователь может изменить права доступа, владельца и группу для любого файла. Для других пользователей имеются ограничения. Они могут только изменять права доступа и группу файлов, которыми владеют, но не могут менять владельца файлов. Несуперпользователи могут также изменять группу файла только на одну из групп,
к которым они принадлежат сами.

Функции chmod(), chgrp() и chown() не работают в Windows.

Добавлено: 22 Июля 2018 18:37:59 Добавил: Андрей Ковальчук

Получение информации о файле

Задача
Необходимо прочитать метаданные файла, например права доступа и имя владельца.

Решение
Вызовите функцию stat(), которая возвращает массив информации о файле:

$info = stat('harpo.php');


Обсуждение
Функция stat() возвращает массив информации о файле и с числовыми, и со строковыми индексами.

Элемент mode возвращенного массива содержит права доступа в виде целого числа по основанию 10. Это сбивает с толку, поскольку права доступа обычно выражаются либо символически (т. е. вывод команды ls -rw-r--r--), либо в виде восьмеричного числа (т. е. 0644). Конвертировать права доступа в более понятный формат позволяет функция base_convert(), преобразующая права доступа в восьмеричное число:

$file_info = stat('/tmp/session.txt');
$permissions = base_convert($file_info['mode'],10,8);


В результате получим восьмеричное число из шести цифр. Например, если команда ls показывает о /tmp/session.txt следующее:

-rw-rw-r-- 1 sklar sklar 12 Oct 23 17:55 /tmp/session.txt


то значение элемента $file_info['mode'] равно 33204, а значение переменной $permissions равно 100664. Последние три цифры (664) определяют права доступа к файлу: пользователя (чтение и запись), группы (чтение и запись) и всех остальных (чтение). Третья цифра, 0, означает, что для файла не установлен ни бит смены идентификатора пользователя (setuid), ни бит смены идентификатора группы (setgid). Крайнее слева число 10 означает, что это обычный файл (а не сокет, символическая ссылка или другой специальный файл).

Поскольку функция stat() возвращает массив как с числовым, так и со строковым индексами, то в результате выполнения цикла foreach по этому массиву мы получаем две копии каждого значения. И поэтому надо обратиться к циклу for от элемента 0 до элемента 12 возвращенного массива.

Вызов функции stat() для символической ссылки возвращает информацию о файле, на который указывает эта символическая ссылка. Для получения информации о самой символической ссылке применяется функция lstat().

Функция fstat() аналогична рассмотренной stat() и принимает дескриптор файла (возвращенный функцией fopen() или popen()) в качестве аргумента. Функцию fstat() можно применять только для локальных файлов и нельзя – к URL, передаваемым функции fopen().
PHP-функция stat() осуществляет дорогостоящий базовый системный вызов stat(2). Для минимизации накладных расходов PHP кэширует результаты вызова stat(2). Поэтому если вызвать функцию stat() для файла, изменить его права и снова вызвать функцию stat() для этого же файла, то будут получены те же самые результаты. Чтобы заставить PHP перегрузить метаданные файла, вызовите функцию clearstatcache(), которая сбросит информацию буфера PHP. Этот же кэш нужен PHP и для других функций, возвращающих метаданные:

file_exists(), fileatime(), filectime(), filegroup(), fileinode(), filemtime(), fileowner(),
fileperms(), filesize(), filetype(), fstat(), is_dir(), is_executable(), is_file(), is_link(), is_readable(), is_writable() и lstat().

Добавлено: 22 Июля 2018 18:36:46 Добавил: Андрей Ковальчук

Получение и установка меток даты/времени файла

Задача
Необходимо узнать время последнего изменения или доступа к файлу либо обновить время доступа или изменения; например, вы хотите показать время последнего изменения каждой страницы вашего веб-сайта.

Решение
Функции fileatime(), filemtime() и filectime() возвращают время последнего доступа, время изменения файла и модификации его метаданных:

$last_access = fileatime('larry.php');
$last_modification = filemtime('moe.php');
$last_change = filectime('curly.php');


Функция touch() изменяет время модификации файла:

touch('shemp.php'); // устанавливаем время модификации,
// равным текущему времени
touch('joe.php',$timestamp); // устанавливаем время модификации,
// равным $timestamp

Обсуждение
Функция fileatime() возвращает время последнего открытия файла на чтение или запись, функция filemtime() – время последнего изменения содержимого файла, а функция filectime() – время последнего изменения содержимого или метаданных файла (таких как владелец
или права доступа). Каждая функция возвращает время в виде метки даты/времени UNIX.
Время модификации файла может быть обновлено с помощью функции touch(). Без второго аргумента функция touch() устанавливает время модификации, равным текущим дате и времени. Чтобы установить время модификации файла в определенное значение, передайте
функции touch() в качестве второго аргумента это значение в виде метки даты/времени UNIX.

Следующий код выводит время последнего обновления страницы веб-сайта:

print "Last Modified: ".strftime('%c',filemtime
($_SERVER['SCRIPT_FILENAME']));

Добавлено: 22 Июля 2018 18:35:36 Добавил: Андрей Ковальчук