Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
107 changes: 107 additions & 0 deletions compiler/composer.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -4,18 +4,102 @@

#include "compiler/composer.h"

#include <filesystem>
#include <fstream>

#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<std::string> collect_php_class_names(const std::string &filepath) {
std::ifstream f(filepath, std::ios::binary);
if (!f) {
return {};
}
std::string content{std::istreambuf_iterator<char>(f), std::istreambuf_iterator<char>()};

std::vector<Token> tokens = php_text_to_tokens(content);

std::vector<std::string> 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<std::string>(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<std::string>(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<std::string, std::string> &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;

Expand Down Expand Up @@ -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;
}
Expand Down Expand Up @@ -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": {
Expand All @@ -167,6 +264,10 @@ void ComposerAutoloader::load_file(const std::string &pkg_root, bool is_root_fil
// "file.php",
// <...>
// ],
// "classmap": [
// "src/",
// <...>
// ],
// }
// }

Expand Down Expand Up @@ -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);
Expand Down
7 changes: 7 additions & 0 deletions compiler/composer.h
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand All @@ -63,6 +68,8 @@ class ComposerAutoloader : private vk::not_copyable {
PsrMap autoload_psr4_;
PsrMap autoload_psr0_;
std::map<std::string, std::string> autoload_psr0_classmap_;
// classmap: class_name_with_slashes -> absolute .php file path
std::unordered_map<std::string, std::string> autoload_classmap_;
std::unordered_set<std::string> deps_;

std::string autoload_filename_;
Expand Down
6 changes: 6 additions & 0 deletions compiler/data/composer-json-data.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down
14 changes: 13 additions & 1 deletion compiler/data/composer-json-data.h
Original file line number Diff line number Diff line change
Expand Up @@ -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);

Expand All @@ -64,4 +72,8 @@ class ComposerJsonData {

// "autoload/files" and "autoload-dev/files", e.g. [ {"file.php", true}, ... ]
std::vector<AutoloadFileItem> 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<AutoloadClassmapEntry> autoload_classmap;
};
4 changes: 4 additions & 0 deletions compiler/pipes/collect-required-and-classes.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
{
"name": "kphp/testing-classmap",
"version": "1.0.0",
"autoload": {
"classmap": ["src/"]
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
<?php

require_once 'vendor/autoload.php';

Logger::log("hello");

$client = new Api\ApiClient();
echo $client->request("/users") . "\n";
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
<?php

namespace Api;

class ApiClient {
public function request(string $endpoint): string {
return "GET " . $endpoint;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
<?php

class Logger {
public static function log(string $message): void {
echo "LOG: " . $message . "\n";
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
<?php

// autoload.php — simple classmap loader for test fixture
// KPHP recognises this file by path and skips it; PHP executes it.

spl_autoload_register(function(string $class): void {
static $map = [
'Logger' => __DIR__ . '/../src/Logger.php',
'Api\\ApiClient' => __DIR__ . '/../src/Api/ApiClient.php',
];
if (isset($map[$class])) {
require_once $map[$class];
}
});
7 changes: 7 additions & 0 deletions tests/python/tests/composer/test_composer.py
Original file line number Diff line number Diff line change
Expand Up @@ -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"),
})