Skip to content

Commit

Permalink
[compiler] implement composer classmap autoloading
Browse files Browse the repository at this point in the history
`autoload.classmap` is another popular method of classes
autoloading in composer.

Implementing this feature increases the number of composer packages
that can be used by KPHP code.

Classmap works like this:
for a given files list (dirs or regular files),
collect all files with `.inc` and `.php` extension recursively and
build autoloading maps for them.

Unlike PSR4, classmap doesn't require any conventions.
A file can have multiple classes, its name could be anything,
namespaces can be arbitrary as well.

It's hard to implement this feature in KPHP without actually parsing the php files.
As a compromise, we're scanning the classmap folders and
require all files found during the composer autoload file inclusion.

Fixes #49
  • Loading branch information
quasilyte committed Sep 2, 2022
1 parent f85374e commit 8e19991
Show file tree
Hide file tree
Showing 11 changed files with 114 additions and 3 deletions.
2 changes: 1 addition & 1 deletion compiler/compiler.cmake
Expand Up @@ -240,7 +240,7 @@ add_executable(kphp2cpp ${KPHP_COMPILER_DIR}/kphp2cpp.cpp)
target_include_directories(kphp2cpp PUBLIC ${KPHP_COMPILER_DIR})

prepare_cross_platform_libs(COMPILER_LIBS yaml-cpp re2)
set(COMPILER_LIBS vk::kphp2cpp_src vk::tlo_parsing_src vk::popular_common ${COMPILER_LIBS} fmt::fmt OpenSSL::Crypto pthread)
set(COMPILER_LIBS -lstdc++fs vk::kphp2cpp_src vk::tlo_parsing_src vk::popular_common ${COMPILER_LIBS} fmt::fmt OpenSSL::Crypto pthread)

target_link_libraries(kphp2cpp PRIVATE ${COMPILER_LIBS})
target_link_options(kphp2cpp PRIVATE ${NO_PIE})
Expand Down
49 changes: 49 additions & 0 deletions compiler/composer.cpp
Expand Up @@ -6,6 +6,8 @@

#include <yaml-cpp/yaml.h> // using YAML parser to handle JSON files

#include <filesystem>

#include "common/algorithms/contains.h"
#include "common/wrappers/fmt_format.h"
#include "compiler/kphp_assert.h"
Expand Down Expand Up @@ -123,6 +125,42 @@ class Psr0Loader : public PsrLoader {
};
} // namespace

bool ComposerAutoloader::is_classmap_file(const std::string &filename) const noexcept {
return vk::contains(classmap_files_, filename);
}

void ComposerAutoloader::scan_classmap(const std::string &filename) {
// supporting the real composer classmap is cumbersome: it requires full PHP parsing to
// fetch all classes from files (the filename doesn't have to follow any conventions);
// we could also invoke php interpreter over vendor/composer/autoload_classmap.php to
// print a JSON dump of the generated classmap and then decode that, but then
// it will be impossible to compile a kphp program that uses a classmap without php interpreter;
// as an alternative, we add all classmap files to auto-required lists that will be
// included along "autoload.files" files, if some classes are not needed, they will be
// discarded after we compute actually used symbols
//
// this approach works well as long as there is no significant side effects related to
// the files being autoloaded (otherwise those side effects will trigger at different point in time)

const auto add_classmap_file = [&](const std::string &filename) {
classmap_files_.insert(filename);
files_to_require_.emplace_back(filename);
};

auto file_info = std::filesystem::status(filename);
kphp_error(file_info.type() != std::filesystem::file_type::not_found,
fmt_format("can't find {} classmap file", filename));
if (file_info.type() == std::filesystem::file_type::directory) {
for (const auto &entry : std::filesystem::directory_iterator(filename)) {
scan_classmap(entry.path().string());
}
} else if (file_info.type() == std::filesystem::file_type::regular) {
if (vk::string_view(filename).ends_with(".php") || vk::string_view(filename).ends_with(".inc")) {
add_classmap_file(filename);
}
}
}

std::string ComposerAutoloader::psr_lookup_nocache(const PsrMap &psr, const std::string &class_name, bool transform_underscore) {
std::string prefix = class_name;
// we start from a longest prefix and then try to match it
Expand Down Expand Up @@ -253,6 +291,11 @@ void ComposerAutoloader::load_file(const std::string &pkg_root, bool is_root_fil
// "": "fallback-dir/",
// <...>
// },
// "classmap": [
// "src/",
// "lib/file.php",
// <...>
// ],
// "files": [
// "file.php",
// <...>
Expand Down Expand Up @@ -315,6 +358,12 @@ void ComposerAutoloader::add_autoload_section(const YAML::Node &autoload, const
Psr0Loader psr0_loader{autoload, autoload_psr0_classmap_, autoload_psr0_, pkg_root};
psr0_loader.load();

// https://getcomposer.org/doc/04-schema.md#classmap
const auto &classmap_src = autoload["classmap"];
for (const auto &elem : classmap_src) {
scan_classmap(pkg_root + elem.as<std::string>());
}

if (require_files) {
// files that are required by the composer-generated autoload.php
// https://getcomposer.org/doc/04-schema.md#files
Expand Down
5 changes: 5 additions & 0 deletions compiler/composer.h
Expand Up @@ -48,6 +48,8 @@ class ComposerAutoloader : private vk::not_copyable {
return filename == autoload_filename_;
}

bool is_classmap_file(const std::string &filename) const noexcept;

const std::vector<std::string> &get_files_to_require() const noexcept {
return files_to_require_;
}
Expand All @@ -60,11 +62,14 @@ class ComposerAutoloader : private vk::not_copyable {

static std::string psr_lookup_nocache(const PsrMap &psr, const std::string &class_name, bool transform_underscore = false);

void scan_classmap(const std::string &filename);

bool use_dev_;
PsrMap autoload_psr4_;
PsrMap autoload_psr0_;
std::map<std::string, std::string> autoload_psr0_classmap_;
std::unordered_set<std::string> deps_;
std::unordered_set<std::string> classmap_files_;

std::string autoload_filename_;
std::vector<std::string> files_to_require_;
Expand Down
3 changes: 2 additions & 1 deletion compiler/data/class-data.cpp
Expand Up @@ -42,7 +42,8 @@ void ClassData::set_name_and_src_name(const std::string &full_name) {
std::string namespace_name = pos == std::string::npos ? "" : full_name.substr(0, pos);
std::string class_name = pos == std::string::npos ? full_name : full_name.substr(pos + 1);

this->can_be_php_autoloaded = file_id && namespace_name == file_id->namespace_name && class_name == file_id->short_file_name;
this->can_be_php_autoloaded = file_id && ((namespace_name == file_id->namespace_name && class_name == file_id->short_file_name) ||
(G->get_composer_autoloader().is_classmap_file(file_id->file_name)));
this->can_be_php_autoloaded |= this->is_builtin();

this->is_lambda = vk::string_view{full_name}.starts_with("Lambda$") || vk::string_view{full_name}.starts_with("ITyped$");
Expand Down
11 changes: 11 additions & 0 deletions tests/python/tests/composer/php/classmap/classmap_lib.php
@@ -0,0 +1,11 @@
<?php

namespace ClassmapLib\Classes;

class ClassmapClass1 {
public string $value = 'a';
}

class ClassmapClass2 {
public int $value = 51;
}
@@ -0,0 +1,7 @@
<?php

class ClassmapNoNamespace {
public function f() {
var_dump('f()');
}
}
9 changes: 9 additions & 0 deletions tests/python/tests/composer/php/classmap/dir/classmap.php
@@ -0,0 +1,9 @@
<?php

namespace OtherClassmap;

class OtherClassmapClass {
public function g() {
var_dump('g()');
}
}
7 changes: 6 additions & 1 deletion tests/python/tests/composer/php/composer.json
Expand Up @@ -27,7 +27,12 @@
"multi2/src/"
],
"": "fallback"
}
},
"classmap": [
"classmap/classmap_lib.php",
"classmap/classmap_no_namespace.php",
"classmap/dir/"
]
},
"autoload-dev": {
"psr-4": {
Expand Down
10 changes: 10 additions & 0 deletions tests/python/tests/composer/php/index.php
Expand Up @@ -10,6 +10,7 @@
use MultiDir\MultiClass1; // from ./multi1
use MultiDir\MultiClass2; // from ./multi2
use Fallback1\Fallback2\FallbackClass; // from ./fallback
use ClassmapLib\Classes\ClassmapClass1; // from ./classmap/classmap_lib.php

class MyController extends Feed\Helpers\BaseController {}

Expand All @@ -19,6 +20,15 @@ class MyController extends Feed\Helpers\BaseController {}
new FallbackClass();
new UtilsFallback(); // from ./packages/utils/utils-fallback/src

$classmap_c1 = new ClassmapClass1();
var_dump($classmap_c1->value);
$classmap_c2 = new \ClassmapLib\Classes\ClassmapClass2();
var_dump($classmap_c2->value);
$classmap_c3 = new ClassmapNoNamespace();
$classmap_c3->f();
$classmap_c4 = new \OtherClassmap\OtherClassmapClass();
$classmap_c4->g();

$controller = new Controller();

$t = new FeedTester();
Expand Down
Expand Up @@ -6,4 +6,9 @@
$baseDir = dirname($vendorDir);

return array(
'ClassmapLib\\Classes\\ClassmapClass1' => $baseDir . '/classmap/classmap_lib.php',
'ClassmapLib\\Classes\\ClassmapClass2' => $baseDir . '/classmap/classmap_lib.php',
'ClassmapNoNamespace' => $baseDir . '/classmap/classmap_no_namespace.php',
'Composer\\InstalledVersions' => $vendorDir . '/composer/InstalledVersions.php',
'OtherClassmap\\OtherClassmapClass' => $baseDir . '/classmap/dir/classmap.php',
);
Expand Up @@ -50,12 +50,21 @@ class ComposerStaticInit9539f736c30192217f7a8384c08769a6
1 => __DIR__ . '/..' . '/vk/utils/utils-fallback/src',
);

public static $classMap = array (
'ClassmapLib\\Classes\\ClassmapClass1' => __DIR__ . '/../..' . '/classmap/classmap_lib.php',
'ClassmapLib\\Classes\\ClassmapClass2' => __DIR__ . '/../..' . '/classmap/classmap_lib.php',
'ClassmapNoNamespace' => __DIR__ . '/../..' . '/classmap/classmap_no_namespace.php',
'Composer\\InstalledVersions' => __DIR__ . '/..' . '/composer/InstalledVersions.php',
'OtherClassmap\\OtherClassmapClass' => __DIR__ . '/../..' . '/classmap/dir/classmap.php',
);

public static function getInitializer(ClassLoader $loader)
{
return \Closure::bind(function () use ($loader) {
$loader->prefixLengthsPsr4 = ComposerStaticInit9539f736c30192217f7a8384c08769a6::$prefixLengthsPsr4;
$loader->prefixDirsPsr4 = ComposerStaticInit9539f736c30192217f7a8384c08769a6::$prefixDirsPsr4;
$loader->fallbackDirsPsr4 = ComposerStaticInit9539f736c30192217f7a8384c08769a6::$fallbackDirsPsr4;
$loader->classMap = ComposerStaticInit9539f736c30192217f7a8384c08769a6::$classMap;

}, null, ClassLoader::class);
}
Expand Down

0 comments on commit 8e19991

Please sign in to comment.