phpFashion

Na navigaci | Klávesové zkratky

How to Summarily Name Classes and Interfaces?

A naming conundrum: how to collectively refer to classes and interfaces? For instance, what should you call a variable that could contain either a class or an interface name? What should be used instead of $class?

One might consider the term type ($type), but this is quite generic because a type can also be a string or an array. From the perspective of the language, a type could be something more complex, such as ?array. Moreover, it's debatable what constitutes the type of an object: is it the class name, or is it object?

However, there indeed exists a collective term for classes and interfaces: it is the word class.

How so?

  1. From a declaration standpoint, an interface is essentially a stripped-down class. It can only contain public abstract methods, which also implies that objects cannot be created. Therefore, interfaces are a subset of classes. If something is a subset, we can refer to it by the name of the superset. Just as a human is a mammal, an interface is a class.
  2. Nevertheless, there's also the usage perspective. A class can inherit from only one class but can implement multiple interfaces. However, this limitation pertains to classes, not to the interfaces themselves. Similarly, a class cannot inherit from a final class, but we still perceive the final class as a class. Also, if a class can implement multiple interfaces (i.e., classes, see 1.), we still regard them as classes.

And what about traits? They simply do not belong here, as they do not exist from an OOP standpoint.

Thus, the issue of naming classes and interfaces together is resolved. Let’s simply call them classes.

classes + interfaces = classes

Well, but a new problem has arisen. How to refer to classes that are not interfaces? That is, their complement. What was referred to at the beginning of the article as classes. Non-interface? Or “implementations”#Class_vs._type)? ?

That's an even bigger nut to crack. It’s a tough nut indeed. You know what, let's forget that interfaces are also classes and again pretend that every OOP identifier is either a class or an interface. It will be easier.

7 years ago v rubrice PHP


Everything About Output Buffering in PHP

And what you won't read in the documentation, including a security patch and advice on speeding up server response without slowing it down.

Output buffering allows the output of a PHP script (primarily from the echo function) to be stored in memory (i.e., a buffer) instead of being sent immediately to the browser or terminal. This is useful for various purposes.

Preventing Output to the Screen:

ob_start();  // enables output buffering
$foo->bar();  // all output goes only to the buffer
ob_end_clean();  // clears the buffer and ends buffering

Capturing Output into a Variable:

ob_start();  // enables output buffering
$foo->render();  // output goes only to the buffer
$output = ob_get_contents();  // saves the buffer content into a variable
ob_end_clean();  // clears the buffer and ends buffering

The pair ob_get_contents() and ob_end_clean() can be replaced by a single function ob_get_clean(), which removes end from the name but indeed turns off output buffering:

$output = ob_get_clean();  // saves the buffer content into variable and disables buffering

In the given examples, the buffer content did not reach the output at all. If you want to send it to the output instead, you should use ob_end_flush() instead of ob_end_clean(). To simultaneously get the buffer content, send it to the output, and end buffering, there is also a shortcut: ob_get_flush().

You can empty the buffer at any time without ending it using ob_clean() (clears it) or ob_flush() (sends it to the output):

ob_start();  // enables output buffering
$foo->bar();  // all output goes only to the buffer
ob_clean();  // clears the buffer content, but buffering remains active
$foo->render(); // output still goes to the buffer
ob_flush(); // sends the buffer to the output
$none = ob_get_contents();  // the buffer content is now an empty string
ob_end_clean();  // disables output buffering

Output written to php://output is also sent to the buffer, while buffers can be bypassed by writing to php://stdout (or STDOUT), which is available only under CLI, i.e., when running scripts from the command line.

Nesting

Buffers can be nested, so while one buffer is active, calling ob_start() activates a new buffer. Thus, ob_end_flush() and ob_flush() send the buffer content not to the output but to the parent buffer. Only when there is no parent buffer does the content get sent to the actual output, i.e., the browser or terminal.

Therefore, it is important to end buffering, even if an exception occurs during the process:

ob_start();
try {
    $foo->render();
} finally {  // finally available from PHP 5.5
    ob_end_clean(); // or ob_end_flush()
}

Buffer Size

The buffer can also “speed up page generation (I haven't measured this, but it sounds logical)” by not sending every single echo to the browser, but a larger amount of data (e.g., 4kB). Just call at the beginning of the script:

ob_start(null, 4096);

When the buffer size exceeds 4096 bytes (the so-called chunk size), a flush is performed automatically, i.e., the buffer is emptied and sent out. The same can be achieved by setting the output_buffering directive. It is ignored in CLI mode.

But beware, starting buffering without specifying the size, i.e., simply with ob_start(), will cause the page not to be sent gradually but only after it is fully rendered, making the server appear very slow!

HTTP Headers

Output buffering has no effect on sending HTTP headers, which are processed by a different path. However, thanks to buffering, headers can be sent even after some output has been printed, as it is still held in the buffer. This is a side effect you shouldn't rely on, as there is no certainty when the output will exceed the buffer size and be sent.

Security Hole

When the script ends, all unclosed buffers are outputted. This can be considered an unpleasant security hole if, for example, you prepare sensitive data in the buffer not intended for output and an error occurs. The solution is to use a custom handler:

ob_start(function () { return ''; });

Handlers

You can attach a custom handler to output buffering, i.e., a function that processes the buffer content before sending it out:

ob_start(
    function ($buffer, $phase) { return strtoupper($buffer); }
);
echo 'Hello';
ob_end_flush(); // 'HELLO' is sent to the output

Functions ob_clean() or ob_end_clean() will call the handler but discard the output without sending it out. The handler can detect which function is called and respond accordingly. The second parameter $phase is a bitmask (from PHP 5.4):

  • PHP_OUTPUT_HANDLER_START when the buffer is opened
  • PHP_OUTPUT_HANDLER_FINAL when the buffer is closed
  • PHP_OUTPUT_HANDLER_FLUSH when ob_flush() is called (but not ob_end_flush() or ob_get_flush())
  • PHP_OUTPUT_HANDLER_CLEAN when ob_clean(), ob_end_clean(), and ob_get_clean() are called
  • PHP_OUTPUT_HANDLER_WRITE when an automatic flush occurs

The start, final, and flush (or clean) phases can occur simultaneously, distinguished by the binary operator &:

if ($phase & PHP_OUTPUT_HANDLER_START) { ... }
if ($phase & PHP_OUTPUT_HANDLER_FLUSH) { ... }
elseif ($phase & PHP_OUTPUT_HANDLER_CLEAN) { ... }
if ($phase & PHP_OUTPUT_HANDLER_FINAL) { ... }

The PHP_OUTPUT_HANDLER_WRITE phase occurs only if the buffer has a size (chunk size) and that size was exceeded. This is the mentioned automatic flush. Note, the constant PHP_OUTPUT_HANDLER_WRITE has a value of 0, so you can't use a bit test, but:

if ($phase === PHP_OUTPUT_HANDLER_WRITE) { ... }

A handler doesn't have to support all operations. When activating with ob_start(), you can specify the bitmask of supported operations as the third parameter:

  • PHP_OUTPUT_HANDLER_CLEANABLE – allows calling ob_clean() and related functions
  • PHP_OUTPUT_HANDLER_FLUSHABLE – allows calling ob_flush()
  • PHP_OUTPUT_HANDLER_REMOVABLE – buffer can be ended
  • PHP_OUTPUT_HANDLER_STDFLAGS – combines all three flags, the default behavior

This applies even to buffering without a custom handler. For example, if I want to capture the output into a variable, I don't set the PHP_OUTPUT_HANDLER_FLUSHABLE flag, preventing the buffer from being (accidentally) sent to the output with ob_flush(). However, it can still be done with ob_end_flush() or ob_get_flush(), which somewhat defeats the purpose.

Similarly, not setting the PHP_OUTPUT_HANDLER_CLEANABLE flag should prevent the buffer from being cleared, but again it doesn't work.

Finally, not setting PHP_OUTPUT_HANDLER_REMOVABLE makes the buffer user-undeletable; it turns off only when the script ends. An example of a handler that should be set this way is ob_gzhandler, which compresses output, thus reducing volume and increasing data transfer speed. Once this buffer is opened, it sends the HTTP header Content-Encoding: gzip, and all subsequent output must be compressed. Removing the buffer would break the page.

The correct usage is:

ob_start(
    'ob_gzhandler',
    16000, // without chunk size, the server would not send data gradually
    PHP_OUTPUT_HANDLER_FLUSHABLE // but not removable or cleanable
);

You can also enable output compression by setting the zlib.output_compression directive, which turns on buffering with a different handler (not sure how it differs specifically), but it lacks the flag to be non-removable. Since it's good to compress the transfer of all text files, not just PHP-generated pages, it's better to activate compression directly on the HTTP server side.


Are you looking for php_ssh2.dll?

PHP ssh2 thread safe binaries for Microsoft Windows:

8 years ago v rubrice PHP


PHP 5.4 Short Arrays Converter

Command-line script to convert between array() and PHP 5.4's short syntax []. It uses native PHP tokenizer, so conversion is safe. The script was successfully tested against thousands of PHP files.

Download from GitHub

To convert all *.php and *.phpt files in whole directory recursively or to convert a single file use:

php convert.php <directory | file>

To convert source code from STDIN and print the output to STDOUT use:

php convert.php < input.php > output.php

To convert short syntax [] to older long syntax array() use option --reverse:

php convert.php --reverse [<directory | file>]

9 years ago v rubrice PHP


Frameworks are a Relic

The way applications are developed in PHP has dramatically transformed over the last 5 years. Initially, we moved away from pure PHP and learned to use frameworks. Later, Composer arrived, enabling library installations from the command line. Now, we are witnessing the end of frameworks as we know them.

Monolithic frameworks are gradually disintegrating into separate (decoupled) components. This transition offers several advantages. While previously using just one part of a framework was difficult or impossible, today you can simply install its component. The development cycle of individual components can vary. They have their own repositories, issue trackers, and can have their own development teams.

You can update components to new versions continuously, without waiting for the next version of the entire framework. Alternatively, you may decide not to update a certain component, perhaps due to a BC break.

The meaning of the word “framework” is shifting; talking about versions is almost obsolete. Instead of using framework XYZ in version 2.3.1, you use a set of components in various versions that work together.

Splitting a framework into components is quite complex. For Nette, it took 2 years and was completed last year. The adoption of Composer and the consistent use of dependency injection were absolutely essential. Nette now consists of over 20 separate repositories, and the original one retains only a single class.

All major frameworks, such as Symfony, Zend, Laravel, or CakePHP, are divided into components, though one step remains to be completed: splitting into separate repositories (instead of a workaround like Git subtree split). Zend promises to do this in version 2.5; we'll see what happens with Symfony.

Composing Nette

Through this long introduction, I wanted to lead you to the idea that viewing Nette as a framework in any specific version is outdated. It's smarter to approach it as a set of components.

That is, instead of declaring a dependency on nette/nette, you should declare dependencies on specific components. This is now being done by Sandbox. For the foundation of a future application, you can also use the Nette Web Project, which is a minimalist version of the Sandbox. Download it using

composer create-project nette/web-project

and remove from composer.json the components you do not need. This will speed up Composer operations.

Bug fixes will also reach you faster. Once an error is fixed, you can immediately tag a new version for the relevant component, whereas the release cycle for the entire framework is much slower.

If you are creating add-ons for Nette, do not hesitate and immediately replace the dependency on nette/nette with a list of actually required components.

Of course, new versions of the framework will continue to be released as before, require nette/nette will still work, and for version 2.3, distributions in ZIP archives will also be released. But their significance will gradually diminish.

9 years ago v rubrice Nette


Nette is the 3rd Most Popular Framework!

In the Best PHP Framework for 2015 survey conducted by SitePoint magazine, Nette secured an impressive 3rd place. Thank you very much to everyone who voted; I truly did not expect such a fantastic result. [perex]

What I find gratifying is that users seem to be satisfied with Nette, otherwise, they probably wouldn't have sent their votes. And, of course, the fact that Nette has thus made itself known in a world where it is less known due to language barriers.

Another interesting aspect of the results is realizing how many PHP frameworks are actually used, that there are other popular “local frameworks,” and that there are still many who do not use any framework at all.

9 years ago v rubrice Nette


Supercharged DI Heart for Your Applications

One of the most interesting parts of Nette, praised even by users of other frameworks, is the Dependency Injection Container (hereinafter referred to as Nette DI). See how easily you can use it anywhere, even outside of Nette. [perex]

Let's consider an application for sending newsletters. I've simplified the code of the individual classes to the core. Here's an object representing an email:

class Mail
{
	public $subject;
	public $message;
}

Someone who knows how to send it:

interface Mailer
{
	function send(Mail $mail, $to);
}

We add support for logging:

interface Logger
{
	function log($message);
}

And finally, a class that manages the distribution of newsletters:

class NewsletterManager
{
	private $mailer;
	private $logger;

	function __construct(Mailer $mailer, Logger $logger)
	{
		$this->mailer = $mailer;
		$this->logger = $logger;
	}

	function distribute(array $recipients)
	{
		$mail = new Mail;
		...
		foreach ($recipients as $recipient) {
			$this->mailer->send($mail, $recipient);
		}
		$this->logger->log(...);
	}
}

The code respects Dependency Injection, meaning each class only works with the variables that we have passed to it. We also have the option to implement Mailer and Logger in our own way, for example like this:

class SendMailMailer implements Mailer
{
	function send(Mail $mail, $to)
	{
		mail($to, $mail->subject, $mail->message);
	}
}

class FileLogger implements Logger
{
	private $file;

	function __construct($file)
	{
		$this->file = $file;
	}

	function log($message)
	{
		file_put_contents(this->file, $message . "\n", FILE_APPEND);
	}
}

The DI container is the supreme architect that can create individual objects (referred to in DI terminology as services) and assemble and configure them precisely according to our needs.

A container for our application could look something like this:

class Container
{
	private $logger;
	private $mailer;

	function getLogger()
	{
		if (!$this->logger) {
			$this->logger = new FileLogger('log.txt');
		}
		return $this->logger;
	}

	function getMailer()
	{
		if (!$this->mailer) {
			$this->mailer = new SendMailMailer;
		}
		return this->mailer;
	}

	function createNewsletterManager()
	{
		return new NewsletterManager($this->getMailer(), $this->getLogger());
	}
}

The implementation looks this way so that:

  • the individual services are created only when needed (lazy)
  • a double call to createNewsletterManager always uses the same logger and mailer objects

Create an instance of Container, let it produce a manager, and you can start spamming users with newsletters:

$container = new Container;
$manager = $container->createNewsletterManager();
$manager->distribute(...);

The essence of Dependency Injection is that no class depends on the container. Therefore, we can easily replace it with another, perhaps with a container generated by Nette DI.

Nette DI

Nette DI is indeed a container generator. We instruct it (usually) using configuration files, and perhaps this configuration generates roughly the same as the Container class:

services:
	- FileLogger( log.txt )
	- SendMailMailer
	- NewsletterManager

A significant advantage is the brevity of the notation. Additionally, we can add more and more dependencies to individual classes often without needing to modify the configuration.

Nette DI generates actual PHP container code. It is therefore extremely fast, and the programmer knows exactly what it does and can even step through it.

The container might have tens of thousands of lines in the case of large applications, and maintaining something like that manually would probably not be possible.

Deploying Nette DI into our application is very easy. First, we install it using Composer (because downloading ZIPs is so outdated):

composer require nette/di

We save the above configuration in a file config.neon and use the class Nette\DI\ContainerLoader to create the container:

$loader = new Nette\DI\ContainerLoader(__DIR__ . '/temp');
$class = $loader->load(function($compiler) {
	$

compiler->loadConfig(__DIR__ . '/config.neon');
});
$container = new $class;

and then again let it create the NewsletterManager object and we can start sending emails:

$manager = $container->getByType('NewsletterManager');
$manager->distribute(['john@example.com', ...]);

But back to ContainerLoader for a moment. The mentioned syntax is subordinate to one thing: speed. The container is generated once, its code is written to cache (directory __DIR__ . '/temp'), and for subsequent requests, it is just loaded from here. Therefore, the loading of the configuration is placed into a closure in the $loader->load() method.

During development, it is useful to activate the auto-refresh mode, where the container is automatically regenerated if any class or configuration file changes. Just mention true as the second argument in the ContainerLoader constructor.

As you can see, using Nette DI is definitely not limited to applications written in Nette, you can deploy it anywhere with just 3 lines of code. Try playing with it, the whole example is available on GitHub.

9 years ago v rubrice PHP


Farewell and Goodbye, Nette 2.0 & PHP 5.2

It has been three years since Nette 2.0.0 was released. It was a groundbreaking version that concluded several years of development and introduced features that are indispensable in Nette development today.

  • Dependency Injection
  • NEON format
  • Debug Bar extendable with custom panels
  • Unobtrusive JavaScript validation in forms
  • New API for extending Latte
  • New structure of namespaces and classes
  • Introduced the database layer Nette Database and NDBT
  • And a completely rewritten documentation

Coincidentally, at that time, major version twos of significant frameworks such as Zend and Symfony were also released. It's worth mentioning that unlike these frameworks, Nette did not abandon users of its previous versions. It did not draw a thick line between versions but instead tried to preserve compatibility as much as possible. For example, users received a tool that replaced old class names with new ones in their source codes, etc.

PHP 5.2

The 2.0 series still supported PHP 5.2, including PHP 5.2.0, which was indeed painful. This version of PHP was one of the less successful, yet Debian had it pre-installed, and conservative administrators refused to upgrade it.

Interestingly, since 2010, Nette was written purely in PHP 5.3 with all its features like namespaces and anonymous functions. The (two) versions for PHP 5.2 were created using a machine converter. This converter not only replaced class names with non-namespaced variants but also managed to rewrite anonymous functions and handle various other differences, such as the inability to use func_get_args() as a function parameter, etc.

Example of code in PHP 5.3:

	/**
	 * Caches results of function/method calls.
	 * @param  mixed
	 * @param  array  dependencies
	 * @return Closure
	 */
	public function wrap($function, array $dependencies = null)
	{
		$cache = $this;
		return function() use ($cache, $function, $dependencies) {
			$key = array($function, func_get_args());
			$data = $cache->load($key);
			if ($data === null) {
				$data = $cache->save($key, Nette\Callback::create($function)->invokeArgs($key[1]), $dependencies);
			}
			return $data;
		};
	}

And the converted code for PHP 5.2:

	/**
	 * Caches results of function/method calls.
	 * @param  mixed
	 * @param  array  dependencies
	 * @return NClosure
	 */
	public function wrap($function, array $dependencies = null)
	{
		$cache = $this;
		return create_function('',
			'extract($GLOBALS[0]['.array_push($GLOBALS[0], array('cache'=>$cache,'function'=> $function,'dependencies'=> $dependencies)).'-1], EXTR_REFS);
			$_args=func_get_args(); $key = array($function, $_args);
			$data = $cache->load($key);
			if ($data === null) {
				$data = $cache->save($key, NCallback::create($function)->invokeArgs($key[1]), $dependencies);
			}
			return $data;
		');
	}

Dependency Injection

Looking back, the most significant contribution of Nette 2.0 was Dependency Injection. But as the old saying goes:

Dependency Injection is no simple matter. It really isn't. It's a concept not everyone is well-versed in.

DI replaced the previously used object Service Locator and its static version, the Environment class, completely overturning the way applications were designed. It brought a qualitative leap to a new level. Therefore, rewriting an application that used Environment to Dependency Injection is extremely challenging, as it essentially means redesigning it better and from scratch.

End of Life

The first day of the year 2014 saw the release of Nette 2.0.14. Yes, it was a neat coincidence ? This marked the end of the 2.0 series, and the series entered a one-year phase of critical issues only, where only severe bugs were fixed. Today, this phase is ending. A few days ago, Nette 2.0.18, the definitively last version of this series and also the last version for PHP 5.2, was released.

So farewell and goodbye!

(The 2.1 series now enters the *critical issues only phase

. During 2015, only severe bugs will be fixed.)*

10 years ago v rubrice PHP


Composer: How to Install in Different Ways

Composer, the most important tool for PHP developers, offers three methods to install packages:

  • local composer require vendor/name
  • global composer global require vendor/name
  • as a project composer create-project vendor/name

Local Installation

Local installation is the most common. If I have a project where I want to use Tracy, I enter in the project's root directory:

composer require tracy/tracy

Composer will update (or create) the composer.json file and download Tracy into the vendor subfolder. It also generates an autoloader, so in the code, I just need to include it and can use Tracy right away:

require __DIR__ . '/vendor/autoload.php';
Tracy\Debugger::enable();

As a Project

A completely different situation arises when, instead of a library whose classes I use in my project, I install a tool that I only run from the command line.

An example might be ApiGen for generating clear API documentation. In such cases, the third method is used:

composer create-project apigen/apigen

Composer will create a new folder (and thus a new project) apigen and download the entire tool and install its dependencies.

It will have its own composer.json and its own vendor subfolder.

This method is also used for installations like Nette Sandbox or CodeChecker. However, testing tools such as Nette Tester or PHPUnit are not installed this way because we use their classes in tests, calling Tester\Assert::same() or inheriting from PHPUnit_Framework_TestCase.

Unfortunately, Composer allows tools like ApiGen to be installed using composer require without even issuing a warning.

This is equivalent to forcing two developers, who don't even know each other and who work on completely different projects, to share the same vendor folder. To this one might say:

  • For heaven's sake, why would they do that?
  • It just can't work!

Indeed, there is no reasonable reason to do it, it brings no benefit, and it will stop working the moment there is a conflict of libraries used. It's just a matter of time, like building a house of cards that will sooner or later collapse. One project will require library XY in version 1.0, another in version 2.0, and at that point, it will stop working.

Global Installation

The difference between option 1) and 2), i.e., between composer require and composer global require, is that it involves not two, but ten different developers and ten unrelated projects. Thus, it is nonsensical squared.

Because composer global is a bad solution every time, there is no use case where it would be appropriate. The only advantage is that if you add the global vendor/bin directory to your PATH, you can easily run libraries installed this way.

Summary

  • Use composer require vendor/name if you want to use library classes.
  • Never use composer global require vendor/name!
  • Use composer create-project vendor/name for tools called only from the command line.

Note: npm uses a different philosophy due to JavaScript's capabilities, installing each library as a “separate project” with its own vendor (or node_modules) directory. This prevents version conflicts. In the case of npm, global installations of tools, like LESS CSS, are very useful and convenient.

10 years ago v rubrice PHP


How to Effectively Type on a Keyboard

typing on a keyboard

Learning to type using all ten fingers and mastering the correct finger placement is undoubtedly a great skill. But between us, I've spent my entire life “pecking” at the keyboard with two fingers, and when typing, I place far greater importance on something else. And that is the layout of the keyboard.

Webmasters, programmers, or copywriters encounter the problem that many frequently used characters are either completely missing or less accessible on the Czech keyboard. Typographic characters suffer the most, like Czech quotation marks „ “, ellipsis …, multiplication sign ×, copyright ©, etc. Typically, this is resolved by switching between two keyboards, Czech and English, and memorizing a million Alt-number shortcuts to substitute for the missing characters. Either way, it greatly hinders creativity. Could there be a better way?

Custom Keyboard Layout

The solution is to create your own keyboard layout. I perfected mine about ten years ago, and it's suitable for programmers, web designers, and copywriters, containing all the essential typographic tricks like dash, double and single quotation marks, etc., intuitively placed. Of course, you can customize the layout further, as described below.

All typographic characters are accessible via the right Alt, or AltGr. The layout is intuitive:

  • Czech double quotation marks „“ AltGr-< AltGr->
  • Czech single quotation marks ‚‘ AltGr-Shift-< AltGr-Shift->
  • non-breaking space AltGr-spacebar
  • multiplication sign × AltGr-X
  • ellipsis … AltGr-D (dot)
  • en dash – AltGr-hyphen
  • em dash — AltGr-Shift-hyphen
  • copyright © AltGr-C
  • trademark ™ AltGr-T
  • AltGr-E
  • ø AltGr-O

And so on, you can view the entire layout on the images.

Download: klávesnice dg v5 (for Windows)

How to Create Your Own Keyboard Layout?

It's easy and fun. Directly from Microsoft, download the magical and well-hidden program Microsoft Keyboard Layout Creator (requires .NET Framework to run).

Upon launching, you'll see an “empty” keyboard, meaning no layout is defined yet. Starting from scratch isn't ideal, so find the Load existing keyboard command in the menu and load one of the standard layouts (like the classic Czech keyboard).

Microsoft Keyboard Layout Creator

For each key, you can define the character that is typed when the key is pressed alone and also when combined with modifiers (i.e., Shift, Ctrl+Alt (right Alt), right Alt +Shift, Caps Lock, and Shift+Caps Lock). You can also designate a key as a dead key, meaning the character is typed only after pressing another key. This is how accents like háček and čárka function on the Czech keyboard.

The real gem is exporting the finished keyboard layout. The result is a full-fledged keyboard driver, including an installation program. So, you can upload your keyboard to the internet and install it on other computers.

10 years ago v rubrice Web


phpFashion © 2004, 2024 David Grudl | o blogu

Ukázky zdrojových kódů smíte používat s uvedením autora a URL tohoto webu bez dalších omezení.