diff --git a/compiler/composer.cpp b/compiler/composer.cpp index dda13c9f18..c88b2b23a6 100644 --- a/compiler/composer.cpp +++ b/compiler/composer.cpp @@ -4,18 +4,102 @@ #include "compiler/composer.h" +#include +#include + #include "common/algorithms/contains.h" #include "common/wrappers/fmt_format.h" #include "compiler/compiler-core.h" #include "compiler/data/composer-json-data.h" #include "compiler/kphp_assert.h" +#include "compiler/lexer.h" +#include "compiler/token.h" static bool file_exists(const std::string &filename) { return access(filename.c_str(), F_OK) == 0; }; +// Scans a single PHP file using KPHP's own lexer and returns all fully-qualified +// class names declared in it. Uses '/' as the namespace separator (the same +// convention used by the rest of this file). +// +// Requires lexer_init() to have been called beforehand (guaranteed by the +// compiler pipeline: lexer_init() runs before init_composer_class_loader()). +static std::vector collect_php_class_names(const std::string &filepath) { + std::ifstream f(filepath, std::ios::binary); + if (!f) { + return {}; + } + std::string content{std::istreambuf_iterator(f), std::istreambuf_iterator()}; + + std::vector tokens = php_text_to_tokens(content); + + std::vector result; + std::string ns_prefix; // e.g. "Foo/Bar/" or "" + + for (size_t i = 0; i < tokens.size(); ++i) { + const auto type = tokens[i].type(); + + // track namespace declarations: "namespace Foo\Bar;" + if (type == tok_namespace && i + 1 < tokens.size() && tokens[i + 1].type() == tok_func_name) { + std::string ns = static_cast(tokens[i + 1].str_val); + std::replace(ns.begin(), ns.end(), '\\', '/'); + ns_prefix = ns + "/"; + ++i; + continue; + } + + // detect class/interface/trait declarations + // the lexer produces: tok_class / tok_interface / tok_trait + // optionally preceded by tok_abstract or tok_final (which we can just skip) + if (type == tok_class || type == tok_interface || type == tok_trait) { + if (i + 1 < tokens.size() && tokens[i + 1].type() == tok_func_name) { + result.push_back(ns_prefix + static_cast(tokens[i + 1].str_val)); + ++i; + } + continue; + } + } + + return result; +} + +// Scans 'path' (a directory or a single .php file) for PHP class declarations +// and inserts the found class_name→file_path pairs into 'out'. +// Class names use '/' as the namespace separator. +static void scan_classmap_path(const std::string &path, + std::unordered_map &out) { + namespace fs = std::filesystem; + + std::error_code ec; + fs::path fsp{path}; + + if (fs::is_regular_file(fsp, ec) && fsp.extension() == ".php") { + for (const auto &cls : collect_php_class_names(path)) { + out.emplace(cls, path); + } + return; + } + + if (fs::is_directory(fsp, ec)) { + const auto opts = fs::directory_options::skip_permission_denied; + for (const auto &entry : fs::recursive_directory_iterator(fsp, opts, ec)) { + if (!entry.is_regular_file()) { + continue; + } + if (entry.path().extension() != ".php") { + continue; + } + const std::string file_path = entry.path().string(); + for (const auto &cls : collect_php_class_names(file_path)) { + out.emplace(cls, file_path); + } + } + } +} + std::string ComposerAutoloader::psr_lookup_nocache(const PsrMap &psr, const std::string &class_name, bool transform_underscore) { std::string prefix = class_name; @@ -102,6 +186,14 @@ std::string ComposerAutoloader::psr0_lookup(const std::string &class_name) const return file; } +std::string ComposerAutoloader::classmap_lookup(const std::string &class_name) const { + auto it = autoload_classmap_.find(class_name); + if (it != autoload_classmap_.end() && file_exists(it->second)) { + return it->second; + } + return ""; +} + void ComposerAutoloader::set_use_dev(bool v) { use_dev_ = v; } @@ -153,6 +245,11 @@ void ComposerAutoloader::load_file(const std::string &pkg_root, bool is_root_fil // "file.php", // <...> // ], + // "classmap": [ + // "src/", // directory to scan for class declarations + // "lib/Foo.php", // single file + // <...> + // ], // } // "autoload-dev": { // "psr-4": { @@ -167,6 +264,10 @@ void ComposerAutoloader::load_file(const std::string &pkg_root, bool is_root_fil // "file.php", // <...> // ], + // "classmap": [ + // "src/", + // <...> + // ], // } // } @@ -207,6 +308,12 @@ void ComposerAutoloader::load_file(const std::string &pkg_root, bool is_root_fil } } + for (const auto &classmap_entry : composer_json->autoload_classmap) { + if (!classmap_entry.is_dev || use_dev) { + scan_classmap_path(classmap_entry.path, autoload_classmap_); + } + } + for (const auto &require : composer_json->require) { if (is_root_file && (!require.is_dev || use_dev)) { deps_.insert(require.package_name); diff --git a/compiler/composer.h b/compiler/composer.h index 76dddf17e4..6312613351 100644 --- a/compiler/composer.h +++ b/compiler/composer.h @@ -42,6 +42,11 @@ class ComposerAutoloader : private vk::not_copyable { std::string psr4_lookup(const std::string &class_name) const; std::string psr0_lookup(const std::string &class_name) const; + // classmap_lookup returns the absolute .php file path for a class that was + // found by scanning "autoload/classmap" directories/files (see #49); + // class_name uses '/' as namespace separator (same convention as psr4/psr0) + std::string classmap_lookup(const std::string &class_name) const; + // is_autoload_file reports whether the specified absolute filename // is a composer-generated autoload.php file bool is_autoload_file(const std::string &filename) const noexcept { @@ -63,6 +68,8 @@ class ComposerAutoloader : private vk::not_copyable { PsrMap autoload_psr4_; PsrMap autoload_psr0_; std::map autoload_psr0_classmap_; + // classmap: class_name_with_slashes -> absolute .php file path + std::unordered_map autoload_classmap_; std::unordered_set deps_; std::string autoload_filename_; diff --git a/compiler/data/composer-json-data.cpp b/compiler/data/composer-json-data.cpp index 705096b918..4962139f7c 100644 --- a/compiler/data/composer-json-data.cpp +++ b/compiler/data/composer-json-data.cpp @@ -143,6 +143,12 @@ class ComposerJsonParser { parse_composer_json_autoload_file(y_filename, is_autoload_dev); } } + if (const auto &y_classmap = y_autoload["classmap"]) { + for (const auto &y_path : y_classmap) { + std::string abs_path = composer_json_dir->full_dir_name + as_string(y_path); + out->autoload_classmap.emplace_back(ComposerJsonData::AutoloadClassmapEntry{std::move(abs_path), is_autoload_dev}); + } + } } // parse composer.json "require" and "require-dev" diff --git a/compiler/data/composer-json-data.h b/compiler/data/composer-json-data.h index f23962a5ac..fa0a8b752a 100644 --- a/compiler/data/composer-json-data.h +++ b/compiler/data/composer-json-data.h @@ -43,7 +43,15 @@ class ComposerJsonData { std::string file_name; bool is_dev; // whether it's in "autoload-dev" }; - + + // "autoload/classmap" entry: a directory or .php file listed under composer.json "classmap" + // composer.json format: "autoload": { "classmap": ["src/", "lib/Foo.php"] } + // Each entry is a raw path from composer.json (absolute after resolving against the package root). + // The actual class→file scanning is performed by ComposerAutoloader when loading the file. + struct AutoloadClassmapEntry { + std::string path; // absolute path: a directory to scan or a single .php file + bool is_dev; // whether it's in "autoload-dev" + }; explicit ComposerJsonData(const std::string &json_filename); @@ -64,4 +72,8 @@ class ComposerJsonData { // "autoload/files" and "autoload-dev/files", e.g. [ {"file.php", true}, ... ] std::vector autoload_files; + + // "autoload/classmap" and "autoload-dev/classmap" (see #49) + // Each entry is a raw path (directory or .php file) to be scanned by ComposerAutoloader. + std::vector autoload_classmap; }; diff --git a/compiler/pipes/collect-required-and-classes.cpp b/compiler/pipes/collect-required-and-classes.cpp index 761f585828..c9cb73efa9 100644 --- a/compiler/pipes/collect-required-and-classes.cpp +++ b/compiler/pipes/collect-required-and-classes.cpp @@ -50,6 +50,10 @@ class CollectRequiredPass final : public FunctionPassBase { file->is_loaded_by_psr0 = true; return; // required from the composer autoload PSR-0 path } + if (const auto &classmap_filename = composer.classmap_lookup(file_name); !classmap_filename.empty()) { + require_file(classmap_filename, false); + return; // required from the composer autoload classmap + } } // fallback to the default class autoloading scheme; diff --git a/tests/python/tests/composer/php/test_autoload_classmap/composer.json b/tests/python/tests/composer/php/test_autoload_classmap/composer.json new file mode 100644 index 0000000000..db98726806 --- /dev/null +++ b/tests/python/tests/composer/php/test_autoload_classmap/composer.json @@ -0,0 +1,7 @@ +{ + "name": "kphp/testing-classmap", + "version": "1.0.0", + "autoload": { + "classmap": ["src/"] + } +} diff --git a/tests/python/tests/composer/php/test_autoload_classmap/index.php b/tests/python/tests/composer/php/test_autoload_classmap/index.php new file mode 100644 index 0000000000..a1c4e4ed3b --- /dev/null +++ b/tests/python/tests/composer/php/test_autoload_classmap/index.php @@ -0,0 +1,8 @@ +request("/users") . "\n"; diff --git a/tests/python/tests/composer/php/test_autoload_classmap/src/Api/ApiClient.php b/tests/python/tests/composer/php/test_autoload_classmap/src/Api/ApiClient.php new file mode 100644 index 0000000000..37c3320e9e --- /dev/null +++ b/tests/python/tests/composer/php/test_autoload_classmap/src/Api/ApiClient.php @@ -0,0 +1,9 @@ + __DIR__ . '/../src/Logger.php', + 'Api\\ApiClient' => __DIR__ . '/../src/Api/ApiClient.php', + ]; + if (isset($map[$class])) { + require_once $map[$class]; + } +}); diff --git a/tests/python/tests/composer/test_composer.py b/tests/python/tests/composer/test_composer.py index 6adb212837..456a85f6d9 100644 --- a/tests/python/tests/composer/test_composer.py +++ b/tests/python/tests/composer/test_composer.py @@ -37,3 +37,10 @@ def test_autoload_files(self): "KPHP_COMPOSER_ROOT": os.path.join(self.test_dir, "php/test_autoload_files"), "KPHP_COMPOSER_AUTOLOAD_DEV": "1", }) + + def test_classmap_autoloading(self): + self.build_and_compare_with_php( + php_script_path="php/test_autoload_classmap/index.php", + kphp_env={ + "KPHP_COMPOSER_ROOT": os.path.join(self.test_dir, "php/test_autoload_classmap"), + })