The Peachpie compiler platform has already proven to be able to run massive real world applications without modifications. We have already tested WordPress, an older version of CodeIgniter and PHPUnit, but now it’s time for another massive PHP project: MediaWiki.
What is Peachpie?
Peachpie is a complete re-implementation of the PHP language to be executed on the .NET runtime instead of the standard Zend engine.
Why, you ask? The objectives are described in detail here, and you can also take a look at some of the real-world use cases. In short, the primary goals are performance and security considerations, as well as a both-way interoperability between PHP and .NET.
There have historically been several projects that attempted to tackle the same issues as Peachpie does. Notably, Facebook dedicated a considerable amount of resources to its HipHop project, which later morphed into the HipHop Virtual Machine (or HHVM). The second successful project was Phalanger, the predecessor of Peachpie, which achieved a full compatibility with PHP up to version 5.4 and was thus capable of running entire real-world applications.
Peachpie is based on the same concepts as Phalanger, with the key difference that it is built on top of Microsoft’s Roslyn compiler platform, which inherently provides it with powerful performance optimizations and cross-platform capabilities.
Introduction
The following article should be viewed as our best practices when trying to get a PHP project to run on .NET with Peachpie. It describes our workflow and while there are many other ways, this one gives us the necessary flexibility to look into the internals of Peachpie when needed. We can quickly start/stop the server, debug the code and implement missing features as needed.
Peachpie is already capable to run full frameworks and legacy PHP applications with little to no modifications to the original source code. Still, it is a work in progress and only in version 0.9 yet. There are still PHP library functionalities still to be implemented (e.g. cURL, SOAP, object destructors or some tricks with PHP references), and there is lots of space for further optimizations. Please view this article as it is intended – a guide on how to get things running.
Getting set up
The only prerequisite is to have the .NET Core SDK installed. Start by downloading the MediaWiki sources; in this case, we chose to pull them from GitHub. The project depends on composer packages, which we know since we see the composer.json
file in the root folder. As of writing this article, Peachpie does not pull the install composer packages, and we didn’t even try to run the composer on Peachpie just yet. Thus, for this task we use the regular PHP, download composer.phar
and run php composer.phar install --no-dev
.
No need to be worried; in the future, this task will be handled by the Peachpie SDK itself, just like NuGet restore.
A particular characteristic of MediaWiki is that it requires you to download a skin separately (e.g. from https://www.mediawiki.org/wiki/Special:SkinDistributor/Vector). Do this before the installation to make the process easier. Extract the content into the /skins folder. The skin also depends on certain composer packages, so repeat the previous step and run the “composer install” command again within the skin/vector directory.
Firing up the compiler
Now it’s time to involve the compiler. Don’t expect it to work out of the box, after all we are changing the entire runtime and platform. Be prepared to run into issues that will need your attention. This article is meant to guide you through this iterative workflow of getting everything to work.
First, create a project file and place it into the www root of the project, where all the PHP files are located. All the compilation parameters and targets will be placed in there. The compilation itself will be driven by dotnet. If you are familiar with msbuild, you can place the file anywhere you want and customize the process for your needs, but we will describe the simplest way.
There are more ways how to create the project file; you can refer to our get started and create it using dotnet new
command or you can create it manually. For the purposes of our tutorial, use the following text and save it as “mediawiki.msbuildproj” in the root of the project:
[xml]
<Project Sdk=”Microsoft.NET.Sdk”>
<PropertyGroup>
<OutputType>Library</OutputType>
<TargetFrameworks>netstandard2.0</TargetFrameworks>
<AssemblyName>mediawiki</AssemblyName>
</PropertyGroup>
<ItemGroup>
<Compile Include=”**/*.php” />
</ItemGroup>
<ItemGroup>
<DotNetCliToolReference Include=”Peachpie.Compiler.Tools” Version=”0.9.0-CI00828″ />
<PackageReference Include=”Peachpie.NET.Sdk” Version=”0.9.0-CI00828″ PrivateAssets=”Build” />
<PackageReference Include=”Peachpie.Library.Graphics” Version=”0.9.0-CI00828″ />
</ItemGroup>
</Project>
[/xml]
This minimalistic project file specifies that all the files matching the “**/*.php” pattern will be compiled. If you take a look at the references, Peachpie.Compiler.Tools
is actually a reference to the entire compiler. Peachpie.NET.Sdk
defines how to build the project and it also specifies implicit references to Peachpie libraries (like standard functions, classes and built-in constants). Additionally, we specify a reference to Peachpie.Library.Graphics
, which is not included in the SDK yet.
EDIT: Our project file system has changed since this post, as we moved over to msbuild
. Please refer to our documentation in order to se how to set up the project file.
First try
Now we are ready to give it a go. Remember – we actually expect this to fail, mainly because the PHP code tends to be full of bugs and Peachpie can find some of them and it will not allow you to compile and run such type of code. Let’s see what happens though. Run dotnet build
and watch the world burn:
[bash]
dotnet build
[/bash]
Well, that did not quite work yet.
Cleaning up
As expected, the compilation did not succeed right away. Don’t worry, this is exactly as predicted. You will see a lot of different warnings that actually kind of give us hints as to what is going on and you will also get errors. So first of all, let’s get rid of the errors, and let’s also exclude the files we don’t care about from the compilation. Remember, the better you organize the project the better the results you get. In general, having duplicated classes and function declarations, tests or unused declarations slows down the compilation and only confuses the analysis.
Let’s weed out the unnecessary. Add the following code to the <Compile>
element:
[bash]
<Compile Include=”**/*.php” Exclude=”tests/**;**/tests/**;maintenance/**” />
[/bash]
This excludes tests and other non-related source code from the compilation. Now you can run the compilation again:
[bash]
dotnet build
[/bash]
That’s better, but still not quite it yet. There are still a lot of warnings we don’t really care about. Yes, they may reveal an issue in the code, but in this phase we just want to get the code to run, so we don’t really care.
Suppress unnecessary warnings
We can find a lot of warning PHPX0125: Mandatory parameter '...' declared behind optional parameter
. Suppress this warning by adding <NoWarn>PHPX0125</NoWarn>
into the <PropertyGroup>
section.
More error excluding
Let’s get rid of the other errors. First, error PHP2052: Method ...::__toString() must return a string value
. This is a regular fatal error in PHP and the compiler analysis determines that it will occur when you run the code. Usually you would have to fix it, but to keep things simple, we exclude the affected file from the compilation since it is not needed.
Next up: error PHP2044: Type name '...' could not be resolved
. This one tells us that the code is extending a class or an interface that isn’t defined anywhere in the project. This is also a fatal error, which will make this piece of code unusable during run time. Most of these issues are found in vendor tests and unused components. Exclude them by altering the <Compile>
element again:
[xml]
<Compile Include=”**/*.php” Exclude=”tests/**;**/tests/**;maintenance/**;includes/debug/logger/monolog/**;vendorpsrlogPsrLogTest**;skinsVectorvendorcomposerinstallers/**;vendorcssjanuscssjanustest/**;vendorliuggio/statsd-php-clientsrcLiuggioStatsdClientMonolog/**” />
[/xml]
<Compile>
element by suppressing warnings and excluding errors.Get rid of undefined elements
We also don’t need “MergePlugin” for the composer, which implements two interfaces – PluginInterface and EventSubscriberInterface – both of which aren’t defined anywhere in the code (they are in composer.phar, which we don’t include (can’t include yet) in the compilation). Thus, exclude the directory “vendorwikimediacomposer-merge-plugin**”, as well. Again, modify the <Compile>
element to exclude the directory:
[xml]
<Compile Include=”**/*.php” Exclude=”tests/**;**/tests/**;maintenance/**;includes/debug/logger/monolog/**;vendorpsrlogPsrLogTest**;skinsVectorvendorcomposerinstallers/**;vendorwikimediacomposer-merge-plugin**;vendorcssjanuscssjanustest/**;vendorliuggio/statsd-php-clientsrcLiuggioStatsdClientMonolog/**” />
[/xml]
Patch the serious errors
And finally, there is one last error we didn’t handle yet: vendorwikimediaremex-htmlRemexHtmlTreeBuilderForeignAttributes.php(141,11): error PHPX2070: Cannot access parent:: when current class scope has no parent
. This is a serious error and your app would crash if you run this particular line of code. So patch/fix the code for now by changing this line of code from
[php]
$name = parent::key();
[/php]
to
[php]
$name = “”; // parent::key();
[/php]
and you might also want to report this issue to the vendor later. Let’s try again and see how we do this time:
[bash]
dotnet build
[/bash]
Now that looks a lot more promising! We should definitely also take a look at the warnings eventually. Most of them tell us that there are undefined functions and classes. It is important to keep in mind that Peachpie has not implemented the entire PHP ecosystem yet, so we know we will have to implement these eventually, as needed. We will get to all of these warnings as the project progresses. We are finally ready to move on to the next step – running the code.
Running the code
After a successful compilation, we should have the file “mediawiki.dll” in our “bin/Debug/net/netstandard2.0/” folder, which is the entire project compiled into a single DLL file. What to do with it now? First, we need a web server in order for the DLL to handle web requests. The file contains all the compiled functions and classes, so if you are a hardcore C# programmer, you can just reference the project or the DLL and make use of the PHP classes. Let’s see what to do about the web server.
Setting up the web server
We basically have two options: ASP.NET Core (.NET Core 2.0 or later) with a custom made web server or ASP.NET (Full .NET Framework 4.6 or later) running on IIS. The first option is completely multi-platform, including Mac and Linux and almost any kind of .NET hosting. The second option might be easier, although requires you to configure the local IIS or a web application on Azure and it also runs on Windows only.
Internet Information Services (IIS):
If you choose IIS, most of the work relies on a proper configuration and no actual code is required. First, add
<PackageReference Include="Peachpie.RequestHandler" Version="0.9.0-CI00828" />
to the .msbuildproj
file in the <ItemGroup>>
section, change <TargetFrameworks>netstandard2.0</TargetFrameworks>
to net46
, add <OutDir>Bin</OutDir>
into <PropertyGroup>>
, and run dotnet build
again:
[bash]
dotnet build
[/bash]
Now, create your “Web.Config” as follows, telling IIS to handle .php
files with the Peachpie “RequestHandler”, which invokes the scripts compiled in our DLL file:
[xml]
<?xml version=”1.0″ encoding=”UTF-8″?>
?
<configuration>
?
<system.webServer>
<handlers>
<add name=”PhpHandler” path=”*.php” verb=”*”
type=”Peachpie.RequestHandler.RequestHandler, Peachpie.RequestHandler” preCondition=”integratedMode” />
</handlers>
<defaultDocument>
<files>
<clear />
<add value=”index.php” />
</files>
</defaultDocument>
</system.webServer>
?
<!– ASP.NET configuration –>
<system.web>
<httpRuntime requestValidationMode=”2.0″ requestPathInvalidCharacters=”” />
<pages validateRequest=”false” />
<globalization requestEncoding=”UTF-8″ responseEncoding=”UTF-8″ fileEncoding=”” />
<customErrors mode=”Off” />
</system.web>
<runtime>
<assemblyBinding xmlns=”urn:schemas-microsoft-com:asm.v1″>
<dependentAssembly>
<assemblyIdentity name=”System.Collections.Immutable” publicKeyToken=”b03f5f7f11d50a3a” culture=”neutral” />
<bindingRedirect oldVersion=”1.0.0.0-1.2.1.0″ newVersion=”1.2.1.0″ />
</dependentAssembly>
</assemblyBinding>
</runtime>
</configuration>
[/xml]
Notice that we set the default document to “index.php” and we specify .php
files to be handled by the Peachpie RequestHandler. We also disable the “validateRequest” feature, because MediaWiki sends unencoded text content through POST and we are disabling “requestPathInvalidCharacters” since MediaWiki likes to use the double-colon in URLs. Customize this as you see fit, since there are probably more options and more secure ways of achieving the same result.
Now open the IIS Manager and add a new Website pointing to the root of your project. You will probably have to update the project folder security so that the IIS worker process can access it. You will end up with something similar to:
ASP.NET Core:
If instead you choose to go with ASP.NET Core, you can make use of the compiled mediawiki.dll by setting up a web server of your choice. We typically use the open-source Kestrel web server in our workflow. See this article to get more information on how to choose your web server or refer to this guide. You can also check out our samples, where we set up the Kestrel web server.
Open the web site:
Before you can proceed, you will also have to configure the URL rewriting. This depends on the web server you choose and doesn’t have anything to do with Peachpie itself. Now that your web server is configured and the project is compiled, we can move on to running the code. When targeting IIS, simply open the webpage in your browser (http://localhost:5006 in our case). With ASP.NET Core, run your web server project (either on the command line or in Visual Studio/VS Code). Our recommendation is to run it directly from Visual Studio so that debugging starts right out of the box.
Debugging
Now we have a compiled project, but that’s really just half the work. We also have to make sure the runtime works properly. The project shouldn’t misbehave or crash where it isn’t supposed to crash. Normally, the reasons for most problems are that Peachpie either hasn’t implemented something yet or that the implementation behaves differently from legacy PHP. In order to find out what the issue is or to diagnose performance, we attach the .NET debugger to the website process.
In Visual Studio (not VS Code) attach (Debug | Attach to process) to the process w3wp.exe
(that’s the IIS worker process).
In VS Code with a Kestrel web server on .NET Core, simply start debugging by pressing F5. This article, however, doesn’t focus on setting up VSCode to launch your project.
You can now open a random Mediawiki PHP file and actually put a breakpoint in it; you can also watch the Output window pane for various debug messages (this is the equivalent to PHP’s log file), and you can even get breaks on exceptions, watch call stacks, choose what exceptions you are interested in, see local variables, and inspect the .NET representation of the PHP code in general.
After a few clicks through the web site, we have found out that the MediaWiki Parser code implements its lock()
mechanism relying on PHP’s garbage collection. It expects that local variables referencing an object that implements __destruct()
get called deterministically at the end of the function’s local scope. This is important to understand; .NET does not have such type of garbage collection and this behavior will differ. For that reason, stop debugging, stop the web server, and patch the file “includesparserParser.php” by commenting out all the lines:
[php]
//$magicScopeVariable = $this->lock();
[/php]
You can now build the project again and restart your web server. You can still expect some other minor issues and incompatibilities.
Installing MediaWiki
After opening the newly compiled MediaWiki for the first time, the website guides you through the installation process. As of now, we did not experience any other issues – except one: on the first installation page, choose ENGLISH as your language. In Peachpie 0.9.0, there are some minor misbehavior in combination with Unicode texts.
Go through the installation process and remember to run the MySQL server using the credentials you provide during the installation. Personally, I use Docker for development purposes, running MySQL using the following command:
[bash]
docker run –name wikidb -e MYSQL_ROOT_PASSWORD=password -e MYSQL_DATABASE=my_wiki -p 3306:3306 -d mysql
[/bash]
The last step of installation gives you the “LocalSettings.php” file that is supposed to be saved into the MediaWiki root directory. Do exactly that. Since the project source has been changed with the “LocalSettings.php” file, you have to recompile the project. Stop the web server, build the project again and restart it.
Rebuild the project after updating
LocalSettings.php
file.Now you can open MediaWiki in your browser again and low and behold:
Releasing the project
Obviously at this point the project is not ready for a release to production. First, take note that Peachpie is not a completely finished platform, so you can expect some issues and kindly let us know if you experience any. Second, the project should be compiled using the Release configuration! The following command does the trick:
[bash]
dotnet build -c Release
[/bash]
Third, you can actually remove (most) PHP source files from your distribution. The DLL file contains all the PHP files compiled, so any http request and any PHP include will look into the compiled DLL instead of your file system. You only have to be aware of file system functions (e.g. file_exists, fread, etc.), which PHP applications use in general to check whether a file can be included. As of now (Peachpie 0.9.0), these file system functions won’t check the compiled DLL and they look onto the file system just as regular PHP does. As a result, if the project checks PHP files on the file system, you have to keep the affected files in there.
Note: in case of MediaWiki, it is for example LocalSettings.php; even though it is compiled, you have to keep the file on the file system (although it can be an empty file after you compile it :)).
Currently Peachpie emits lots of unnecessary instructions. You should always profile your code (in release), or you can use tools like ILSpy to decompile a compiled DLL file and see for yourself what instructions have been generated and whether Peachpie compiler could generate it better if you provide, for example, a type hint to a function parameter. There are lots of various optimization hints, tips and tricks. We will definitely discuss some of these as part of a future article once it will be relevant.
Conclusion
In this article, we showed you our workflow when getting a project to run on Peachpie. Based on the example of MediaWiki, we explained how to iteratively get past some roadblocks in the compilation phase, successfully compile and run the code, set up a web server of your choice and subsequently install and release your MediaWiki project. We are currently working on a tutorial video for this process as well. Please do let us know if you experience any issues. We hope you found this article useful.