;;; php-project.el --- Project support for PHP application -*- lexical-binding: t; -*- ;; Copyright (C) 2020 Friends of Emacs-PHP development ;; Author: USAMI Kenta ;; Keywords: tools, files ;; URL: https://github.com/emacs-php/php-mode ;; Version: 1.24.0 ;; License: GPL-3.0-or-later ;; This program is free software; you can redistribute it and/or modify ;; it under the terms of the GNU General Public License as published by ;; the Free Software Foundation, either version 3 of the License, or ;; (at your option) any later version. ;; This program is distributed in the hope that it will be useful, ;; but WITHOUT ANY WARRANTY; without even the implied warranty of ;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the ;; GNU General Public License for more details. ;; You should have received a copy of the GNU General Public License ;; along with this program. If not, see . ;;; Commentary: ;; Define project specific functions and variables for PHP application. ;; ;; ## API ;; ;; ### `php-project-get-root-dir()' ;; ;; Return root directory of current buffer file. The root directory is ;; determined by several marker file or directory. ;; ;; ### `php-project-get-bootstrap-scripts()' ;; ;; Return list of path to bootstrap script file. ;; ;; ### `php-project-get-php-executable()' ;; ;; Return path to PHP executable file with the project settings overriding. ;; ;; ### `php-project-get-phan-executable()' ;; ;; Return path to Phan executable file with the project settings overriding. ;; Phan is a static analyzer and LSP server implementation for PHP. ;; See https://github.com/phan/phan ;; ;; ## `.dir-locals.el' support ;; ;; - `php-project-coding-style' ;; - Symbol value of the coding style. (ex. `pear', `psr2') ;; - `php-project-root' ;; - Symbol of marker file of project root. (ex. `git', `composer') ;; - Full path to project root directory. (ex. "/path/to/your-project") ;; - `php-project-bootstrap-scripts' ;; - List of path to bootstrap file of project. ;; (ex. (((root . "vendor/autoload.php") (root . "inc/bootstrap.php"))) ;; - `php-project-php-executable' ;; - Path to project specific PHP executable file. ;; - If you want to use a file different from the system wide `php' command. ;; - `php-project-phan-executable' ;; - Path to project specific Phan executable file. ;; - When not specified explicitly, it is automatically searched from ;; Composer's dependency of the project and `exec-path'. ;; ;;; Code: (eval-when-compile (require 'cl-lib)) (require 'projectile nil t) ;; Constants (defconst php-project-composer-autoloader "vendor/autoload.php") ;; Custom variables (defgroup php-project nil "Major mode for editing PHP code." :tag "PHP Project" :prefix "php-project-" :group 'php) (defcustom php-project-auto-detect-etags-file nil "If `T', automatically detect etags file when file is opened." :tag "PHP Project Auto Detect Etags File" :group 'php-project :type 'boolean) (defcustom php-project-use-projectile-to-detect-root nil "If `T' and projectile-mode is activated, use Projectile for root detection." :tag "PHP Project Use Projectile To Detect Root" :group 'php-project :type 'boolean) ;; Variables (defvar php-project-available-root-files '((projectile ".projectile") (composer "composer.json" "composer.lock") (git ".git") (mercurial ".hg") (subversion ".svn") ;; NOTICE: This method does not detect the top level of .editorconfig ;; However, we can integrate it by adding the editorconfig.el's API. ;;(editorconfig . ".editorconfig") )) ;; Buffer local variables ;;;###autoload (progn (defvar-local php-project-root 'auto "Method of searching for the top level directory. `auto' (default) Try to search file in order of `php-project-available-root-files'. SYMBOL Key of `php-project-available-root-files'. STRING A file/directory name of top level marker. If the string is an actual directory path, it is set as the absolute path of the root directory, not the marker.") (put 'php-project-root 'safe-local-variable #'(lambda (v) (or (stringp v) (assq v php-project-available-root-files)))) (defvar-local php-project-etags-file nil) (put 'php-project-etags-file 'safe-local-variable #'(lambda (v) (or (functionp v) (eq v t) (php-project--eval-bootstrap-scripts v)))) (defvar-local php-project-bootstrap-scripts nil "List of path to bootstrap php script file. The ideal bootstrap file is silent, it only includes dependent files, defines constants, and sets the class loaders.") (put 'php-project-bootstrap-scripts 'safe-local-variable #'php-project--eval-bootstrap-scripts) (defvar-local php-project-php-executable nil "Path to php executable file.") (put 'php-project-php-executable 'safe-local-variable #'(lambda (v) (and (stringp v) (file-executable-p v)))) (defvar-local php-project-phan-executable nil "Path to phan executable file.") (put 'php-project-phan-executable 'safe-local-variable #'php-project--eval-bootstrap-scripts) (defvar-local php-project-coding-style nil "Symbol value of the coding style of the project that PHP major mode refers to. Typically it is `pear', `drupal', `wordpress', `symfony2' and `psr2'.") (put 'php-project-coding-style 'safe-local-variable #'symbolp) (defvar-local php-project-align-lines t "If T, automatically turn on `php-align-mode' by `php-align-setup'.") (put 'php-project-align-lines 'safe-local-variable #'booleanp) (defvar-local php-project-php-file-as-template 'auto " `auto' (default) Automatically switch to mode for template when HTML tag detected in file. `t' Switch all PHP files in that directory to mode for HTML template. `nil' Any .php in that directory is just a PHP script. \(\(PATTERN . SYMBOL)) Alist of file name pattern regular expressions and the above symbol pairs. PATTERN is regexp pattern. ") (put 'php-project-php-file-as-template 'safe-local-variable #'php-project--validate-php-file-as-template) (defvar-local php-project-repl nil "Function name or path to REPL (interactive shell) script.") (put 'php-project-repl 'safe-local-variable #'(lambda (v) (or (functionp v) (php-project--eval-bootstrap-scripts v)))) (defvar-local php-project-unit-test nil "Function name or path to unit test script.") (put 'php-project-unit-test 'safe-local-variable #'(lambda (v) (or (functionp v) (php-project--eval-bootstrap-scripts v)))) (defvar-local php-project-deploy nil "Function name or path to deploy script.") (put 'php-project-deploy 'safe-local-variable #'(lambda (v) (or (functionp v) (php-project--eval-bootstrap-scripts v)))) (defvar-local php-project-build nil "Function name or path to build script.") (put 'php-project-build 'safe-local-variable #'(lambda (v) (or (functionp v) (php-project--eval-bootstrap-scripts v)))) (defvar-local php-project-server-start nil "Function name or path to server-start script.") (put 'php-project-server-start 'safe-local-variable #'(lambda (v) (or (functionp v) (php-project--eval-bootstrap-scripts v))))) ;; Functions (defun php-project--validate-php-file-as-template (val) "Return T when `VAL' is valid list of safe ." (cond ((null val) t) ((memq val '(t auto)) t) ((listp val) (cl-loop for v in val always (and (consp v) (stringp (car v)) (php-project--validate-php-file-as-template (cdr v))))) (t nil))) (defun php-project--eval-bootstrap-scripts (val) "Return T when `VAL' is valid list of safe bootstrap php script." (cond ((stringp val) (and (file-exists-p val) val)) ((eq 'composer val) (let ((path (expand-file-name php-project-composer-autoloader (php-project-get-root-dir)))) (and (file-exists-p path) path))) ((and (consp val) (eq 'root (car val)) (stringp (cdr val))) (let ((path (expand-file-name (cdr val) (php-project-get-root-dir)))) (and (file-exists-p path) path))) ((null val) nil) ((listp val) (cl-loop for v in val collect (php-project--eval-bootstrap-scripts v))) (t nil))) (defun php-project-get-php-executable () "Return path to PHP executable file." (cond ((and (stringp php-project-php-executable) (file-executable-p php-project-php-executable)) php-project-php-executable) ((boundp 'php-executable) php-executable) (t (executable-find "php")))) (defun php-project-get-phan-executable () "Return path to phan executable file." (or (car-safe (php-project--eval-bootstrap-scripts (list php-project-phan-executable (cons 'root "vendor/bin/phan")))) (executable-find "phan"))) (defun php-project-get-file-html-template-type (filename) "Return symbol T, NIL or `auto' by `FILENAME'." (cond ((not php-project-php-file-as-template) nil) ((eq t php-project-php-file-as-template) t) ((eq 'auto php-project-php-file-as-template) 'auto) ((listp php-project-php-file-as-template) (assoc-default filename php-project-php-file-as-template #'string-match-p)) (t (prog1 nil (warn "php-project-php-file-as-template is unexpected format"))))) (defun php-project-apply-local-variables () "Apply php-project variables to local variables." (when (null tags-file-name) (when (or (and php-project-auto-detect-etags-file (null php-project-etags-file)) (eq php-project-etags-file t)) (let ((tags-file (expand-file-name "TAGS" (php-project-get-root-dir)))) (when (file-exists-p tags-file) (setq-local php-project-etags-file tags-file)))) (when php-project-etags-file (setq-local tags-file-name (php-project--eval-bootstrap-scripts php-project-etags-file))))) ;;;###autoload (defun php-project-get-bootstrap-scripts () "Return list of bootstrap script." (let ((scripts (php-project--eval-bootstrap-scripts php-project-bootstrap-scripts))) (if (stringp scripts) (list scripts) scripts))) ;;;###autoload (defun php-project-get-root-dir () "Return path to current PHP project." (if (and (stringp php-project-root) (file-directory-p php-project-root)) php-project-root (php-project--detect-root-dir))) (defun php-project--detect-root-dir () "Return detected project root." (if (and php-project-use-projectile-to-detect-root (bound-and-true-p projectile-mode) (fboundp 'projectile-project-root)) (projectile-project-root default-directory) (let ((detect-method (cond ((stringp php-project-root) (list php-project-root)) ((eq php-project-root 'auto) (cl-loop for m in php-project-available-root-files append (cdr m))) (t (cdr-safe (assq php-project-root php-project-available-root-files)))))) (cl-loop for m in detect-method thereis (locate-dominating-file default-directory m))))) (provide 'php-project) ;;; php-project.el ends here